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

я тестирую код, который выполняет асинхронную обработку с помощью 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.

12 ответов


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

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert…

    dispatch_semaphore_signal(sema);
}];

dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);

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


В дополнение к Семафорной технике, исчерпывающе описанной в других ответах, теперь мы можем использовать 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; кроме того, в этих наборах тестирования мы должны убедиться, что тест не завершается до асинхронного вызова выполняемый.) Это одна из тех редких ситуаций, когда может потребоваться Семафорная техника блокировки основного потока.

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


недавно я снова пришел к этому вопросу и написал следующую категорию на 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…
}];

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

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

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

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

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

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


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

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

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


- (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);
  }];
}];

там же 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 категория on XCTest это позволяет писать код следующим образом:

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

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

__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);

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]];
}

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


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

#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];
}

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

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Swift 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
}