如何等待异步调度块完成?

zou*_*oul 177 unit-testing objective-c grand-central-dispatch

我正在测试一些使用Grand Central Dispatch进行异步处理的代码.测试代码如下所示:

[object runSomeLongOperationAndDo:^{
    STAssert…
}];
Run Code Online (Sandbox Code Playgroud)

测试必须等待操作完成.我目前的解决方案如下:

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

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

[object runSomeLongOperationAndDo:^{
    STAssert…
}];
dispatch_sync(object.queue, ^{});
Run Code Online (Sandbox Code Playgroud)

......但是这可能会暴露太多object.

kpe*_*yua 299

尝试使用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
Run Code Online (Sandbox Code Playgroud)

即使runSomeLongOperationAndDo:判定操作实际上不足以进行线程化并且同步运行,这也应该正常运行.

  • 这段代码对我不起作用.我的STAssert永远不会执行.我必须用`while(dispatch_semaphore_wait(信号量,DISPATCH_TIME_NOW)){[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]替换`dispatch_semaphore_wait(sema,DISPATCH_TIME_FOREVER);`.}` (61认同)
  • 那可能是因为你的完成块被分派到主队列?阻塞队列等待信号量,因此永远不会执行该块.请参阅[此问题](http://stackoverflow.com/questions/10330679),了解如何在不阻塞的情况下调度主队列. (41认同)
  • 这正是我想要的.谢谢!@PeterWarbo不,你没有.使用ARC无需执行dispatch_release() (14认同)
  • 我听了@Zoul&nicktmro的建议.但它看起来会陷入死锁状态.测试用例' - [BlockTestTest testAsync]'开始了.但从未结束 (3认同)
  • 你需要在ARC下发布信号量吗? (3认同)

Rob*_*Rob 29

除了在其他答案中详尽介绍的信号量技术之外,我们现在可以在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];
}
Run Code Online (Sandbox Code Playgroud)

为了未来的读者,虽然调度信号量技术在绝对需要时是一种很棒的技术,但我必须承认,我发现太多新的开发人员,不熟悉良好的异步编程模式,过于迅速地将信号量作为异步的一般机制.例程表现同步.更糟糕的是我见过很多人在主队列中使用这种信号量技术(我们永远不应该阻止生产应用程序中的主队列).

我知道这不是这里的情况(当发布这个问题时,没有像这样的好工具XCTestExpectation;而且,在这些测试套件中,我们必须确保在异步调用完成之前测试没有完成).这是少数几种可能需要阻塞主线程的信号量技术的情况之一.

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


zou*_*oul 27

我最近再次讨论这个问题并写了以下类别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
Run Code Online (Sandbox Code Playgroud)

通过这种方式,我可以轻松地将异步调用与回调转换为测试中的同步调用:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert…
}];
Run Code Online (Sandbox Code Playgroud)


小智 24

一般不要使用任何这些答案,它们通常不会扩展 (这里和那里都有例外,当然)

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

换句话说,重新排列代码,以便没有同步等待结果,而是处理通知状态更改的结果(例如回调/委托协议,可用,离开,错误等).(如果你不喜欢回调地狱,这些可以重构成块.)因为这是如何向应用程序的其余部分公开真实行为而不是隐藏在假外观后面.

相反,使用NSNotificationCenter,使用您的类的回调定义自定义委托协议.如果你不喜欢遍及委托回调,请将它们包装到实现自定义协议的具体代理类中,并将各种块保存在属性中.也可能也提供便利构造函数.

最初的工作稍微多一些,但从长远来看,它将减少可怕的竞争条件和电池谋杀投票的数量.

(不要问一个例子,因为它是微不足道的,我们也不得不花时间去学习Objective-c基础知识.)


Les*_*win 8

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

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });
Run Code Online (Sandbox Code Playgroud)

你要做的是等待使用dispatch_sync空块来同步等待串行调度队列,直到A-Synchronous块完成.


Oli*_*son 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);
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];
Run Code Online (Sandbox Code Playgroud)