首页 文章

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

提问于
浏览
172

我正在测试一些使用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 上暴露太多 .

12 回答

  • 5

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

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

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

  • 0
    - (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);
      }];
    }];
    
  • 29

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

  • 284

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

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

    我知道这不是't the case here (when this question was posted, there wasn'一个很好的工具,如 XCTestExpectation ;此外,在这些测试套件中,我们必须确保在异步调用完成之前测试不会完成 . 这是少数几种可能需要阻塞主线程的信号量技术的情况之一 .

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

  • 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…
    }];
    
  • 2

    Generally don't use any of these answers, they often won't scale (这里和那里有例外,当然)

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

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

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

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

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

  • 22

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

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

    这是我的一个测试的替代方案:

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

    这样做对我来说 .

  • 1

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

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

    void (^nextOperationAfterLongOperationBlock)(void) = ^{
    
    };
    
    [object runSomeLongOperationAndDo:^{
        STAssert…
        nextOperationAfterLongOperationBlock();
    }];
    
  • 8

    斯威夫特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
    }
    

相关问题