如何等待异步分派的块完成?

我正在测试一些使用 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

123913 次浏览

尝试使用 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:认为这个操作实际上不够长,不值得进行线程化,因此应该同步运行,这个操作也应该正确。

我最近又谈到了这个问题,并在 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…
}];

下面是我的一个测试中的一个替代方案:

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

这就是我想要的。

有时,Timeout 循环也很有帮助。您可以等到从异步回调方法获得一些(可能是 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];
}

还有 SenTestingKitAsync可以让你像这样编写代码:

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

(详情请参阅 对象,文章。)因为 Xcode 6在 XCTest上有一个 AsynchronousTesting类别,可以让你像这样编写代码:

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

通常不要使用这些答案中的任何一个,它们通常不会缩放 (当然,这里和那里也有例外)

这些方法与 GCD 的工作原理不兼容,最终将导致死锁和/或通过不停轮询杀死电池。

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

相反,使用 NSNotificationCenter,为类定义带回调的自定义委托协议。如果您不喜欢到处使用委托回调函数,那么可以将它们封装到一个具体的代理类中,该代理类实现自定义协议并将各个块保存在属性中。也许还提供了方便的构造函数。

最初的工作稍微多一点,但是从长远来看,它将减少可怕的种族条件和电池谋杀民意调查的数量。

(不要要求例子,因为它是琐碎的,我们不得不投入时间来学习 Objective-c 的基础知识。)

除了在其他答案中详尽介绍的信号量技术之外,我们现在可以在 Xcode 6中使用 XCTest 通过 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这样的好工具; 而且,在这些测试套件中,我们必须确保在异步调用完成之前测试不会结束)。这种情况很少见,可能需要使用信号量技术来阻塞主线程。

因此,我向这个问题的作者道歉,信号量技术对他来说是合理的,我写这个警告给所有那些新的开发人员,他们看到这个信号量技术,并考虑在他们的代码中应用它作为处理异步方法的一般方法: 预先警告,十有八九,信号量技术是 没有在计算异步操作时的最佳方法。相反,要熟悉完成块/闭包模式以及委托协议模式和通知。这些通常是处理异步任务的更好方法,而不是使用信号量使它们同步运行。通常,异步任务被设计成异步行为是有充分理由的,因此应该使用正确的异步模式,而不是试图让它们同步行为。

这里有一个不用信号灯的妙招:

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

您要做的是使用带有空块的 dispatch_sync等待,以便在串行调度队列上同步等待,直到 A-Synsync 块完成。

非常原始的解决方案:

void (^nextOperationAfterLongOperationBlock)(void) = ^{


};


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

迅捷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
}

在运行我的方法之前,我必须等待一个 UIWebView 被加载,我能够通过使用 GCD 结合本线程中提到的信号量方法在主线程上执行 UIWebView 准备好的检查来使它工作。最终代码如下:

-(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


}