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

时间:2010-12-01 16:10:45

标签: objective-c unit-testing grand-central-dispatch

我正在测试一些使用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曝光太多。

13 个答案:

答案 0 :(得分:292)

尝试使用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:确定操作实际上不足以进行线程化并且同步运行,这应该会正常运行。

答案 1 :(得分: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];
}

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

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

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

答案 2 :(得分: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

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

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

答案 3 :(得分:23)

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

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

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

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

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

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

答案 4 :(得分:8)

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

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

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

答案 5 :(得分: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);
  }];
}];

答案 6 :(得分:2)

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

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

(有关详细信息,请参阅objc.io article。)从Xcode 6开始,AsynchronousTesting上有一个XCTest类别,可让您编写如下代码:

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

答案 7 :(得分:1)

以下是我的一项测试的替代方案:

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

答案 8 :(得分:0)

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

这对我有用。

答案 9 :(得分:0)

有时,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];
}

答案 10 :(得分:0)

非常原始的问题解决方案:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

答案 11 :(得分:0)

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

答案 12 :(得分:0)

在运行我的方法之前,我必须等到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

}