我如何等待asynchronous调度的块完成?

我正在testing一些使用Grand Central Dispatch进行asynchronous处理的代码。 testing代码如下所示:

[object runSomeLongOperationAndDo:^{ STAssert… }]; 

testing必须等待操作完成。 我目前的解决scheme如下所示:

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

这看起来有点粗糙,你知道更好的方法吗? 我可以公开队列,然后通过调用dispatch_sync阻塞:

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

…但是这可能会暴露太多的object

尝试使用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:这个行为应该是正确的runSomeLongOperationAndDo:决定该操作实际上并不足够长,以致于不需要线程化并同步运行。

我最近再次遇到这个问题,并在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 

通过这种方式,我可以轻松地将asynchronous调用与callback转换为testing中的同步调用:

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

除了其他答案中涵盖的信号量技术之外,我们现在可以在Xcode 6中使用XCTest来通过XCTestExpectation执行asynchronoustesting。 这在testingasynchronous代码时消除了对信号量的需求。 例如:

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

为了将来的读者,尽pipe调度信号技术在绝对需要的时候是一种很好的技术,但是我必须承认,我看到太多的新开发人员,不熟悉好的asynchronous编程模式,对于制作asynchronous的一般机制而言,过于依赖信号量例程的行为是同步的。 更糟糕的是,我已经看到他们中的很多人在主队列中使用了这个信号量技术(我们不应该阻止生产应用程序中的主队列)。

我知道这不是这种情况(当这个问题发布时,没有像XCTestExpectation这样的好工具;在这些testing套件中,我们必须确保testing不会完成,直到asynchronous调用完成)。 这是阻塞主线程的信号量技术可能是必要的罕见情况之一。

所以,我对这个原始问题的作者表示歉意,对于信号量技术是合理的,我将这个警告写给所有看到这个信号量技术的新开发人员,并且考虑在他们的代码中使用它作为处理asynchronous方法:预先指出十次中有九次,信号量技术并不是joinasynchronous操作时的最佳方法。 相反,请熟悉完成块/封闭模式,以及委托协议模式和通知。 这些通常是处理asynchronous任务的好方法,而不是使用信号来使它们同步。 通常有很好的理由,asynchronous任务被devise为asynchronous行为,所以使用正确的asynchronous模式,而不是试图使他们的行为同步。

一般不使用任何这些答案,他们往往不会扩大规模 (当然,这里有例外)

这些方法与GCD的工作方式不兼容,最终会导致死锁和/或通过不间断轮询来终止电池。

换句话说,重新排列你的代码,使得没有同步等待结果,而是处理状态改变的通知(例如callback/委托协议,可用,离开,错误等)的结果。 (如果你不喜欢callback地狱,这些可以被重构成块。)因为这是如何将真实的行为暴露给应用程序的其余部分,而不是将其隐藏在虚假的外观之后。

相反,使用NSNotificationCenter ,定义一个自定义委托协议与您的类的callback。 如果你不喜欢用委托callback,将它们包装到一个实现自定义协议的具体代理类中,并将各个块保存在属性中。 也许还提供便利的build设者。

最初的工作稍微多一点,但是从长远来看,这会减less糟糕的竞争条件和电池谋杀投票的数量。

(不要要求一个例子,因为它是微不足道的,我们也必须投入时间来学习Objective-C的基础知识。)

这是一个不使用信号量的漂亮技巧:

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

你正在等待使用带有空块的dispatch_sync来同步等待串行调度队列,直到A-Synchronous块已经完成。

 - (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以来, XCTest上有一个AsynchronousTesting类别,可让您编写如下代码:

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

这是我的一个testing的另一种select:

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

这是为我做的。

有时,超时循环也​​是有帮助的。 你可以等待,直到你从asynchronouscallback方法得到一些(可能是BOOL)信号,但是如果没有任何反应,并且你想摆脱那个循环呢? 下面是解决scheme,主要回答上面,但增加了超时。

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