Как мне ждать завершения асинхронно отправленного блока?


180

Я тестирую некоторый код, который выполняет асинхронную обработку с использованием Grand Central Dispatch. Тестовый код выглядит так:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Тесты должны ждать окончания операции. Мое текущее решение выглядит так:

__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
    STAssert
    finished = YES;
}];
while (!finished);

Что выглядит немного грубо, знаете ли вы лучший способ? Я мог бы выставить очередь и затем заблокировать, вызвав dispatch_sync:

[object runSomeLongOperationAndDo:^{
    STAssert
}];
dispatch_sync(object.queue, ^{});

... но это может показаться слишком много на object.

Ответы:


302

Пытаюсь использовать dispatch_semaphore. Это должно выглядеть примерно так:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert

    dispatch_semaphore_signal(sema);
}];

if (![NSThread isMainThread]) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

Это должно вести себя правильно, даже если runSomeLongOperationAndDo:решит, что операция на самом деле не достаточно длинная, чтобы заслужить многопоточность, и вместо этого выполняется синхронно.


61
Этот код не работает для меня. Мой STAssert никогда не будет выполняться. Я должен был заменить dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);сwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
Это, вероятно, потому что ваш блок завершения отправляется в основную очередь? Очередь блокируется в ожидании семафора и поэтому никогда не выполняет блок. Смотрите этот вопрос о диспетчеризации в главной очереди без блокировки.
Зул

3
Я последовал предложению @Zoul & nicktmro. Но похоже, что он заходит в тупик. Тестовый пример '- [BlockTestTest testAsync]' запущен. но никогда не кончалось
NSCry

3
Вам нужно выпустить семафор под ARC?
Питер Варбо

14
это было именно то, что я искал. Спасибо! @PeterWarbo нет, ты не. Использование ARC избавляет от необходимости делать dispatch_release ()
Hulvej

29

В дополнение к семафорной технике, подробно рассмотренной в других ответах, теперь мы можем использовать XCTest в Xcode 6 для выполнения асинхронных тестов через XCTestExpectation. Это устраняет необходимость в семафорах при тестировании асинхронного кода. Например:

- (void)testDataTask
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];

    NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
    NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        XCTAssertNil(error, @"dataTaskWithURL error %@", error);

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
            XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
        }

        XCTAssert(data, @"data nil");

        // do additional tests on the contents of the `data` object here, if you want

        // when all done, Fulfill the expectation

        [expectation fulfill];
    }];
    [task resume];

    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

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

Я знаю, что это не тот случай (когда этот вопрос был опубликован, не было такого хорошего инструмента, как XCTestExpectation; также в этих тестах мы должны убедиться, что тест не завершится, пока не будет выполнен асинхронный вызов). Это одна из тех редких ситуаций, когда может понадобиться техника семафора для блокировки основного потока.

Поэтому, приношу свои извинения автору этого оригинального вопроса, для которого техника семафора является разумной, я пишу это предупреждение всем тем новым разработчикам, которые видят эту технику семафора и рассматривают ее применение в своем коде как общий подход к работе с асинхронными методы: имейте в виду, что в девяти случаях из десяти техника семафоров нелучший подход при включении асинхронных операций. Вместо этого ознакомьтесь с шаблонами завершения / закрытия, а также с шаблонами протоколов делегирования и уведомлениями. Часто это гораздо лучшие способы решения асинхронных задач, чем использование семафоров для синхронного поведения. Обычно есть веские причины, по которым асинхронные задачи были разработаны для асинхронного поведения, поэтому используйте правильный асинхронный шаблон, а не пытайтесь заставить их вести себя синхронно.


1
Я думаю, что это должен быть принятый ответ сейчас. Вот документы также: developer.apple.com/library/prerelease/ios/documentation/...
hris.to

У меня есть вопрос по этому поводу. У меня есть некоторый асинхронный код, который выполняет около десятка вызовов загрузки AFNetworking для загрузки одного документа. Я хотел бы запланировать загрузки на NSOperationQueue. Если я не использую что-то вроде семафора, загрузка документов NSOperationбудет сразу же завершена, и не будет никакой реальной очереди загрузок - они будут в значительной степени выполняться одновременно, чего я не хочу. Семафоры здесь разумны? Или есть лучший способ заставить NSOperations ждать асинхронного завершения других? Или что-то другое?
Бенджон

Нет, не используйте семафоры в этой ситуации. Если у вас есть очередь операций, в которую вы добавляете AFHTTPRequestOperationобъекты, тогда вам нужно просто создать операцию завершения (которую вы сделаете зависимой от других операций). Или используйте группы рассылки. Кстати, вы говорите, что не хотите, чтобы они работали одновременно, и это хорошо, если это то, что вам нужно, но вы платите серьезное снижение производительности, выполняя это последовательно, а не одновременно. Я обычно использую maxConcurrentOperationCount4 или 5.
Роб

28

Недавно я снова пришел к этой проблеме и написал следующую категорию NSObject:

@implementation NSObject (Testing)

- (void) performSelector: (SEL) selector
    withBlockingCallback: (dispatch_block_t) block
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self performSelector:selector withObject:^{
        if (block) block();
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(semaphore);
}

@end

Таким образом, я могу легко превратить асинхронный вызов с обратным вызовом в синхронный в тестах:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert
}];

24

Как правило, не используйте ни один из этих ответов, они часто не масштабируются (конечно, есть исключения здесь и там)

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

Другими словами, переставьте ваш код так, чтобы не было синхронного ожидания результата, а вместо этого имейте дело с результатом, уведомляемым об изменении состояния (например, обратные вызовы / протоколы делегирования, доступность, уход, ошибки и т. Д.). (Они могут быть реорганизованы в блоки, если вам не нравится ад обратного вызова.) Потому что это способ показать реальное поведение остальной части приложения, чем скрыть его за ложным фасадом.

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

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

(Не спрашивайте о примере, потому что это тривиально, и нам пришлось потратить время, чтобы изучить основы target-c.)


1
Это важное предупреждение из-за шаблонов проектирования obj-C и тестируемости тоже
BootMaker

8

Вот изящный трюк, который не использует семафор:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });

Что вы делаете, это ждите, используя dispatch_syncпустой блок для синхронного ожидания в очереди последовательной отправки, пока не завершится блок A-Synchronous.


Проблема с этим ответом состоит в том, что он не решает исходную проблему OP, которая заключается в том, что API, который необходимо использовать, принимает в качестве аргумента completeHandler и немедленно возвращает результат. Вызов этого API внутри асинхронного блока этого ответа немедленно вернется, даже если завершение не выполнено. Тогда блок синхронизации будет выполняться до завершения.
BTRUE

6
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
  NSParameterAssert(perform);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  perform(semaphore);
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  dispatch_release(semaphore);
}

Пример использования:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];

2

Есть также SenTestingKitAsync, который позволяет вам писать код следующим образом:

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess();
    }];
    STFailAfter(2.0, @"Timeout");
}

(См. Статью objc.io для деталей.) И, начиная с Xcode 6, есть AsynchronousTestingкатегория, XCTestкоторая позволяет вам писать код следующим образом:

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];

1

Вот альтернатива одного из моих тестов:

__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];

STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
    success = value != nil;
    [completed lock];
    [completed signal];
    [completed unlock];
}], nil);    
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);

1
В приведенном выше коде есть ошибка. Из NSCondition документации на -waitUntilDate:«Вы должны заблокировать приемник до вызова этого метода». Так -unlockдолжно быть после -waitUntilDate:.
Патрик

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

0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
    // ... your code to execute
    dispatch_semaphore_signal(sema);
}];

while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
    [[NSRunLoop currentRunLoop]
        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}

Это сделало это для меня.


3
хорошо, это вызывает высокое использование процессора, хотя
kevin

4
@ Кевин Да, это гетто опрос, который убьет батарею.

@ Барри, как он потребляет больше батареи. пожалуйста, руководство.
pkc456

@ pkc456 Посмотрите в книге по информатике о различиях между работой опроса и асинхронным уведомлением. Удачи.

2
Четыре с половиной года спустя, благодаря знаниям и опыту, которые я приобрел, я бы не стал рекомендовать свой ответ.

0

Иногда циклы Timeout также полезны. Можете ли вы подождать, пока не получите некоторый (может быть BOOL) сигнал от асинхронного метода обратного вызова, но что, если ответа нет, и вы хотите выйти из этого цикла? Здесь ниже решение, в основном ответ выше, но с добавлением Timeout.

#define CONNECTION_TIMEOUT_SECONDS      10.0
#define CONNECTION_CHECK_INTERVAL       1

NSTimer * timer;
BOOL timeout;

CCSensorRead * sensorRead ;

- (void)testSensorReadConnection
{
    [self startTimeoutTimer];

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {

        /* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
        if (sensorRead.isConnected || timeout)
            dispatch_semaphore_signal(sema);

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];

    };

    [self stopTimeoutTimer];

    if (timeout)
        NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);

}

-(void) startTimeoutTimer {

    timeout = NO;

    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void) stopTimeoutTimer {
    [timer invalidate];
    timer = nil;
}

-(void) connectionTimeout {
    timeout = YES;

    [self stopTimeoutTimer];
}

1
Та же проблема: срок службы батареи не работает.

1
@ Барри Не уверен, даже если вы посмотрели на код. Существует период TIMEOUT_SECONDS, в течение которого, если асинхронный вызов не отвечает, он прерывает цикл. Это взломать, чтобы выйти из тупика. Этот код прекрасно работает, не убивая батарею.
Куля Сим Сим

0

Очень примитивное решение проблемы:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert
    nextOperationAfterLongOperationBlock();
}];

0

Свифт 4:

Используйте synchronousRemoteObjectProxyWithErrorHandlerвместо remoteObjectProxyпри создании удаленного объекта. Больше не нужно семафор.

Приведенный ниже пример вернет версию, полученную от прокси. Без synchronousRemoteObjectProxyWithErrorHandlerнего произойдет сбой (при попытке доступа к недоступной памяти):

func getVersion(xpc: NSXPCConnection) -> String
{
    var version = ""
    if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
    {
        helper.getVersion(reply: {
            installedVersion in
            print("Helper: Installed Version => \(installedVersion)")
            version = installedVersion
        })
    }
    return version
}

-1

Мне нужно подождать, пока загрузится UIWebView, прежде чем запускать мой метод, я смог добиться этого, выполнив проверки готовности UIWebView в главном потоке с использованием GCD в сочетании с методами семафоров, упомянутыми в этом потоке. Конечный код выглядит так:

-(void)myMethod {

    if (![self isWebViewLoaded]) {

            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

            __block BOOL isWebViewLoaded = NO;

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                while (!isWebViewLoaded) {

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        isWebViewLoaded = [self isWebViewLoaded];
                    });

                    [NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s

                }

                dispatch_sync(dispatch_get_main_queue(), ^{
                    dispatch_semaphore_signal(semaphore);
                });

            });

            while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
            }

        }

    }

    //Run rest of method here after web view is loaded

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