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

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

Ответ 1

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

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(semaphore, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

#if !__has_feature(objc_arc)
    dispatch_release(sema);
#endif

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

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

Ответ 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);
  }];
}];

Ответ 7

Theres также 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];

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Ответ 12

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
}

Ответ 13

Мне нужно подождать, пока загрузится 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.5];

                }

                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

}