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

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

293 голосов
/ 01 декабря 2010

Попытка использовать 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: решит, что операция на самом деле не достаточно длинная, чтобы заслужить многопоточность, и вместо этого выполняется синхронно.

29 голосов
/ 29 сентября 2014

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

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

27 голосов
/ 01 июня 2011

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

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

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

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

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

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

(Донне требую примера, потому что это тривиально, и нам пришлось потратить время на изучение основ цели c).

8 голосов
/ 22 апреля 2016

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

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

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

6 голосов
/ 05 августа 2013
- (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 голосов
/ 13 мая 2014

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

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

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

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];
1 голос
/ 12 октября 2012

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

__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);
0 голосов
/ 17 сентября 2018

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
}
0 голосов
/ 05 марта 2018

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

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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