Ожидание выполнения двух асинхронных блоков перед запуском другого блока.


192

При использовании GCD мы хотим подождать, пока два асинхронных блока не будут выполнены и выполнены, прежде чем перейти к следующим шагам выполнения. Каков наилучший способ сделать это?

Мы попробовали следующее, но это не сработало:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
});


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
});

// wait until both the block1 and block2 are done before start block3
// how to do that?

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
});

Смотрите мой ответ для Swift 5, который предлагает до шести различных способов решения вашей проблемы.
Imanou Petit

Ответы:


301

Используйте группы рассылки: см. Здесь пример «Ожидание групп задач, поставленных в очередь» в главе «Очереди рассылки» Руководства по программированию параллелизма Apple Developer Developer Library

Ваш пример может выглядеть примерно так:

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
    NSLog(@"Block1");
    [NSThread sleepForTimeInterval:5.0];
    NSLog(@"Block1 End");
});


dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
    NSLog(@"Block2");
    [NSThread sleepForTimeInterval:8.0];
    NSLog(@"Block2 End");
});

dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
    NSLog(@"Block3");
});

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group);

и мог бы произвести вывод как это:

2012-08-11 16:10:18.049 Dispatch[11858:1e03] Block1
2012-08-11 16:10:18.052 Dispatch[11858:1d03] Block2
2012-08-11 16:10:23.051 Dispatch[11858:1e03] Block1 End
2012-08-11 16:10:26.053 Dispatch[11858:1d03] Block2 End
2012-08-11 16:10:26.054 Dispatch[11858:1d03] Block3

3
Прохладно. Будут ли асинхронные задачи / блоки, когда-то связанные с группой, выполняться последовательно или одновременно? Я имею в виду, предположим, что block1 и block2 теперь связаны с группой. Будет ли block2 ждать, пока block1 будет завершен, прежде чем он сможет начать выполнение?
Том

9
Это зависит от вас. dispatch_group_asyncтак же, как dispatch_asyncс добавленным параметром группы. Поэтому, если вы используете разные очереди для block1 и block2 или планируете их в одной и той же параллельной очереди, они могут работать одновременно; если вы запланируете их в одной последовательной очереди, они будут работать последовательно. Это ничем не отличается от планирования блоков без групп.
Йорн Эйрих

1
Это также относится к выполнению публикации веб-службы?
SleepNot

Вы замечаете, что время не равно времени ожидания, установленному в вашем блоке? почему так будет?
Дэймон Юань,

2
В ARC просто удалите dispatch_release (group);
loretoparisi

272

Расширяя ответ Jörn Eyrich (добавьте его ответ, если вы подтвердите этот ответ), если у вас нет контроля над dispatch_asyncвызовами для ваших блоков, как это может быть в случае асинхронных блоков завершения, вы можете использовать группы GCD, используя dispatch_group_enterиdispatch_group_leave напрямую.

В этом примере мы притворяемся computeInBackground, что это то, что мы не можем изменить (представьте, что это обратный вызов делегата, NSURLConnection completeHandler или что-то еще), и поэтому у нас нет доступа к вызовам диспетчеризации.

// create a group
dispatch_group_t group = dispatch_group_create();

// pair a dispatch_group_enter for each dispatch_group_leave
dispatch_group_enter(group);     // pair 1 enter
[self computeInBackground:1 completion:^{
    NSLog(@"1 done");
    dispatch_group_leave(group); // pair 1 leave
}];

// again... (and again...)
dispatch_group_enter(group);     // pair 2 enter
[self computeInBackground:2 completion:^{
    NSLog(@"2 done");
    dispatch_group_leave(group); // pair 2 leave
}];

// Next, setup the code to execute after all the paired enter/leave calls.
//
// Option 1: Get a notification on a block that will be scheduled on the specified queue:
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"finally!");
});

// Option 2: Block an wait for the calls to complete in code already running
// (as cbartel points out, be careful with running this on the main/UI queue!):
//
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // blocks current thread
// NSLog(@"finally!");

В этом примере computeInBackground: завершение: реализовано как:

- (void)computeInBackground:(int)no completion:(void (^)(void))block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%d starting", no);
        sleep(no*2);
        block();
    });
}

Вывод (с метками времени из прогона):

12:57:02.574  2 starting
12:57:02.574  1 starting
12:57:04.590  1 done
12:57:06.590  2 done
12:57:06.591  finally!

1
@ ɲeuroburɳ Приведенный выше код ожидает основной поток. Я считаю, что это заблокирует основной поток и приведет к тому, что пользовательский интерфейс не будет отвечать до тех пор, пока вся группа не будет завершена. Я рекомендую перенести ожидание в фоновый поток. Например, dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_HIGH, 0)
cbartel

2
@cbartel, хороший улов! Я обновил пример кода, чтобы отразить ваш комментарий. Часто требуется, чтобы обратный вызов находился в главной очереди - в этом случае dispatch_queue_notify, вероятно, он лучше (если время блокировки не гарантировано будет коротким).
uroeuroburɳ

Где я могу освободить группу (то есть dispatch_release (group))? Я не уверен, безопасно ли выпускать в dispatch_group_notify. Но так как этот код запускается после завершения группы, я не уверен, где его выпустить.
GingerBreadMane

Если вы используете ARC, вам не нужно вызывать dispatch_release: stackoverflow.com/questions/8618632/…
uroeuroburɳ

3
Хороший пост , который далее объясняет , что: commandshift.co.uk/blog/2014/03/19/...
Rizon

98

С помощью Swift 5.1 Grand Central Dispatch предлагает множество способов решения вашей проблемы. В соответствии с вашими потребностями вы можете выбрать один из семи шаблонов, показанных в следующих фрагментах игровой площадки.


# 1. Используя DispatchGroup, DispatchGroup's notify(qos:flags:queue:execute:)и DispatchQueue' sasync(group:qos:flags:execute:)

В Руководстве по программированию параллелизма разработчиков Apple говорится оDispatchGroup :

Группы рассылки - это способ блокировать поток, пока не завершится выполнение одной или нескольких задач. Вы можете использовать это поведение в тех местах, где вы не можете добиться прогресса, пока все указанные задачи не будут выполнены. Например, после отправки нескольких задач для вычисления некоторых данных вы можете использовать группу для ожидания этих задач, а затем обработать результаты, когда они будут выполнены.

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

queue.async(group: group) {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async(group: group) {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

group.notify(queue: queue) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 2. Использование DispatchGroup, DispatchGroups wait(), DispatchGroups enter()и DispatchGroupsleave()

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

group.enter()
queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    group.leave()
}

group.enter()
queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    group.leave()
}

queue.async {
    group.wait()
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

Обратите внимание , что вы можете также смешать DispatchGroup wait()с DispatchQueue async(group:qos:flags:execute:)или смешивать DispatchGroup enter()и DispatchGroup leave()с DispatchGroup notify(qos:flags:queue:execute:).


# 3. Использование и «SDispatch​Work​Item​Flags barrierDispatchQueueasync(group:qos:flags:execute:)

Учебное пособие по Grand Central для Swift 4: статья 1/2 от Raywenderlich.com дает определение барьеров :

Диспетчерские барьеры - это группа функций, выполняющих роль узкого места в последовательном стиле при работе с параллельными очередями. Когда вы отправляете DispatchWorkItemв очередь отправки, вы можете установить флаги, чтобы указать, что это должен быть единственный элемент, выполняемый в указанной очереди в течение этого определенного времени. Это означает, что все элементы, отправленные в очередь до барьера диспетчеризации, должны быть выполнены до DispatchWorkItemвыполнения.

Использование:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

queue.async(flags: .barrier) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 4. Используя DispatchWorkItem, Dispatch​Work​Item​Flags's barrierи DispatchQueue' sasync(execute:)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let dispatchWorkItem = DispatchWorkItem(qos: .default, flags: .barrier) {
    print("#3 finished")
}

queue.async(execute: dispatchWorkItem)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 5. Используя DispatchSemaphore, DispatchSemaphore's wait()и DispatchSemaphore' ssignal()

Соруш Ханлоу написал следующие строки в блоге GCD Handbook :

Используя семафор, мы можем заблокировать поток на произвольное количество времени, пока не будет отправлен сигнал из другого потока. Семафоры, как и остальная часть GCD, являются поточно-ориентированными, и их можно запускать из любого места. Семафоры можно использовать, когда есть асинхронный API, который нужно сделать синхронным, но вы не можете его изменить.

Справочник по Apple Developer API также дает следующее обсуждение DispatchSemaphore init(value:​)инициализатора:

Передача нуля для значения полезна, когда двум потокам необходимо согласовать завершение определенного события. Передача значения больше нуля полезна для управления конечным пулом ресурсов, где размер пула равен значению.

Использование:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 0)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    semaphore.signal()
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    semaphore.wait()    
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 6. Использование OperationQueueи Operation«SaddDependency(_:)

Справочник по Apple Developer API гласит Operation​Queue:

Очереди операций используют libdispatchбиблиотеку (также известную как Grand Central Dispatch), чтобы инициировать выполнение своих операций.

Использование:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let blockThree = BlockOperation {
    print("#3 finished")
}

blockThree.addDependency(blockOne)
blockThree.addDependency(blockTwo)

operationQueue.addOperations([blockThree, blockTwo, blockOne], waitUntilFinished: false)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

# 7. Использование OperationQueueи OperationQueues addBarrierBlock(_:)(требуется iOS 13)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

operationQueue.addOperations([blockTwo, blockOne], waitUntilFinished: false)
operationQueue.addBarrierBlock {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

Есть ли решение для асинхронных вызовов без использования group.enter () и group.leave () для каждого (и без семафоров)? Например, если мне нужно дождаться асинхронного запроса к серверу, то после этого дождаться второго асинхронного запроса и так далее. Я читал эту статью avanderlee.com/swift/asynchronous-operations, но я не вижу его простого использования по сравнению с BlockOperation
Woof

58

Другой альтернативой GCD является барьер:

dispatch_queue_t queue = dispatch_queue_create("com.company.app.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{ 
    NSLog(@"start one!\n");  
    sleep(4);  
    NSLog(@"end one!\n");
});

dispatch_async(queue, ^{  
    NSLog(@"start two!\n");  
    sleep(2);  
    NSLog(@"end two!\n"); 
});

dispatch_barrier_async(queue, ^{  
    NSLog(@"Hi, I'm the final block!\n");  
});

Просто создайте параллельную очередь, отправьте два блока, а затем отправьте последний блок с барьером, который заставит его ждать окончания двух других.


Есть ли проблема, если я не использовал сон (4);
Himanth

Нет, конечно, с этим проблем нет. На самом деле, вы практически никогда не хотите sleep()! Я добавил эти sleep()призывы только по педагогическим соображениям, чтобы блоки работали достаточно долго, чтобы вы могли видеть, что они работают одновременно. В этом тривиальном примере, при отсутствии sleep(), эти два блока могут выполняться так быстро, что отправленный блок может начинаться и заканчиваться до того, как вы сможете эмпирически наблюдать за одновременным выполнением. Но не sleep()в своем собственном коде.
Роб

39

Я знаю, что вы спрашивали о GCD, но, если хотите, NSOperationQueueтакже обрабатывает такие вещи действительно изящно, например:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 3");
}];

NSOperation *operation;

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 1");
    sleep(7);
    NSLog(@"Finishing 1");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 2");
    sleep(5);
    NSLog(@"Finishing 2");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

[queue addOperation:completionOperation];

3
Это нормально, когда код внутри NSBlockOperation является синхронным. Но что, если это не так, и вы хотите запустить завершение, когда ваша асинхронная операция завершена?
Грег Малетик

3
@GregMaletic В этом случае я делаю NSOperationподкласс, который является параллельным и устанавливается isFinishedпосле завершения асинхронного процесса. Тогда зависимости работают нормально.
Роб


1
@GregMaletic Да, вы тоже можете использовать это (если dispatch_semaphore_waitэто не происходит в основной очереди и пока ваши сигналы и ожидания сбалансированы). Пока вы не блокируете основную очередь, семафорный подход хорош, если вам не нужна гибкость операций (например, возможность отменить их, возможность контролировать степень параллелизма и т. Д.).
Роб

1
@ Reza.Ab - Если вам нужно, чтобы задача первая завершилась до запуска задачи вторая, добавьте зависимость между этими задачами. Или, если очередь всегда выполняет только одну задачу за раз, сделайте ее последовательной, установив maxConcurrentOperationCountв 1. Вы также можете установить приоритет операций, как qualityOfServiceи queuePriority, но они оказывают гораздо более тонкое влияние на приоритет задачи, чем зависимости и / или степень параллелизма очереди.
Роб

4

Ответы выше все классные, но все они пропустили одну вещь. группа выполняет задачи (блоки) в потоке, куда она вошла, когда вы используете dispatch_group_enter/ dispatch_group_leave.

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      dispatch_async(demoQueue, ^{
        dispatch_group_t demoGroup = dispatch_group_create();
        for(int i = 0; i < 10; i++) {
          dispatch_group_enter(demoGroup);
          [self testMethod:i
                     block:^{
                       dispatch_group_leave(demoGroup);
                     }];
        }

        dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
          NSLog(@"All group tasks are done!");
        });
      });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

это выполняется в созданной параллельной очереди demoQueue. Если я не создаю какую-либо очередь, она запускается в основном потоке .

- (IBAction)buttonAction:(id)sender {
    dispatch_group_t demoGroup = dispatch_group_create();
    for(int i = 0; i < 10; i++) {
      dispatch_group_enter(demoGroup);
      [self testMethod:i
                 block:^{
                   dispatch_group_leave(demoGroup);
                 }];
    }

    dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
      NSLog(@"All group tasks are done!");
    });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

и есть третий способ сделать задачи выполненными в другом потоке:

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      //  dispatch_async(demoQueue, ^{
      __weak ViewController* weakSelf = self;
      dispatch_group_t demoGroup = dispatch_group_create();
      for(int i = 0; i < 10; i++) {
        dispatch_group_enter(demoGroup);
        dispatch_async(demoQueue, ^{
          [weakSelf testMethod:i
                         block:^{
                           dispatch_group_leave(demoGroup);
                         }];
        });
      }

      dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
        NSLog(@"All group tasks are done!");
      });
      //  });
    }

Конечно, как уже упоминалось, вы можете использовать, dispatch_group_asyncчтобы получить то, что вы хотите.


3

Первый ответ, по сути, правильный, но если вы хотите, чтобы самый простой способ достиг желаемого результата, вот отдельный пример кода, демонстрирующий, как это сделать с семафором (так же работают диспетчерские группы за кулисами, JFYI) :

#include <dispatch/dispatch.h>
#include <stdio.h>

main()
{
        dispatch_queue_t myQ = dispatch_queue_create("my.conQ", DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t mySem = dispatch_semaphore_create(0);

        dispatch_async(myQ, ^{ printf("Hi I'm block one!\n"); sleep(2); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ printf("Hi I'm block two!\n"); sleep(4); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER); printf("Hi, I'm the final block!\n"); });
        dispatch_main();
}

7
Два замечания: 1. Вы упускаете dispatch_semaphore_wait. У вас есть два сигнала, поэтому вам нужно два ожидания. Таким образом, ваш блок «завершения» начнется, как только первый блок подаст сигнал семафору, но до того, как закончится другой блок; 2. Учитывая, что это вопрос iOS, я бы не рекомендовал использовать dispatch_main.
Роб

1
Я согласен с Робом. Это не правильное решение. dispatch_semaphore_waitРазблокируют , как только любой из dispatch_semaphore_signalметодов называются. Причина, по которой это может сработать, заключается в том, что printfблоки for «one» и «two» возникают немедленно, а printffor «finally» происходит после ожидания - таким образом, после того, как блок один проспал 2 секунды. Если вы поместите printf после sleepвызовов, вы получите вывод для 'one', затем через 2 секунды для 'finally', затем через 2 секунды для 'two'.
uroeuroburɳ

1

Принятый ответ в кратчайшие сроки:

let group = DispatchGroup()

group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block1
    print("Block1")
    Thread.sleep(forTimeInterval: 5.0)
    print("Block1 End")
})


group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block2
    print("Block2")
    Thread.sleep(forTimeInterval: 8.0)
    print("Block2 End")
})

dispatch_group_notify(group, DispatchQueue.global(qos: .default), {
    // block3
    print("Block3")
})

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group)

0

Swift 4.2 пример:

let group = DispatchGroup.group(count: 2)
group.notify(queue: DispatchQueue.main) {
     self.renderingLine = false
     // all groups are done
}
DispatchQueue.main.async {
    self.renderTargetNode(floorPosition: targetPosition, animated: closedContour) {
        group.leave()
        // first done
    }
    self.renderCenterLine(position: targetPosition, animated: closedContour) {
        group.leave()
        // second done
    }
 }

group.leave()вызвал аварию
Бен

-3

Нельзя сказать, что другие ответы не подходят для определенных обстоятельств, но это один фрагмент, который я всегда использую в Google:

- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {


    if (signInDoneSel) {
        [self performSelector:signInDoneSel];
    }

}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.