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

Posted

技术标签:

【中文标题】如何等待异步调度的块完成?【英文标题】:How do I wait for an asynchronously dispatched block to finish? 【发布时间】:2011-05-18 14:26:33 【问题描述】:

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

【问题讨论】:

【参考方案1】:

尝试使用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: 确定操作实际上不够长,不适合线程化并改为同步运行,这也应该正确运行。

【讨论】:

这段代码对我不起作用。我的 STAssert 永远不会执行。我不得不用while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; 替换dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 那可能是因为你的完成块被分派到主队列了?队列被阻塞等待信号量,因此永远不会执行该块。请参阅this question 关于在主队列上调度而不阻塞。 我听从了@Zoul &nicktmro 的建议。但它看起来将进入死锁状态。测试用例“-[BlockTestTest testAsync]”已启动。但从未结束 ARC下需要释放信号量吗? 这正是我想要的。谢谢! @PeterWarbo 不,你没有。使用 ARC 无需执行 dispatch_release()【参考方案2】:

我最近又来这个问题了,在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】:

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

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

【讨论】:

上述代码有错误。来自NSCondition documentation 为-waitUntilDate: “在调用此方法之前,您必须锁定接收器。”所以-unlock 应该在-waitUntilDate: 之后。 这不适用于使用多线程或运行队列的任何东西。【参考方案4】:
- (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);
  ];
];

【讨论】:

【参考方案5】:
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]];

这是为我做的。

【讨论】:

好吧,它会导致 CPU 使用率过高 @kevin 是的,这是会破坏电池的贫民区民意调查。 @Barry,它是如何消耗更多电池的。请指导。 @pkc456 查看计算机科学书籍,了解轮询和异步通知的工作方式之间的差异。祝你好运。 四年半后,凭借我所获得的知识和经验,我不会推荐我的答案。【参考方案6】:

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

【讨论】:

同样的问题:电池寿命失败。 @Barry 即使您查看了代码也不确定。如果异步调用没有响应,则有 TIMEOUT_SECONDS 时间段,它将中断循环。这就是打破僵局的技巧。这段代码可以完美运行而不会耗尽电池。【参考方案7】:

还有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];

【讨论】:

【参考方案8】:

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

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

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

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

最初的工作稍微多一些,但从长远来看,它将减少糟糕的竞争条件和电池谋杀轮询的数量。

(不要问例子,因为它很琐碎,我们也必须花时间学习objective-c基础知识。)

【讨论】:

由于 obj-C 设计模式和可测试性,这也是一个重要警告【参考方案9】:

除了在其他答案中详尽介绍的信号量技术之外,我们现在可以使用 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 这样的好工具;另外,在这些测试套件中,我们必须确保测试在异步调用之前不会完成已经完成了)。这是可能需要阻塞主线程的信号量技术的罕见情况之一。

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

【讨论】:

我认为这应该是现在公认的答案。以下是文档:developer.apple.com/library/prerelease/ios/documentation/… 我对此有疑问。我有一些异步代码,它执行大约十几个 AFNetworking 下载调用来下载单个文档。我想在NSOperationQueue 上安排下载。除非我使用信号量之类的东西,否则文档下载NSOperations 将立即完成,并且不会有任何真正的下载排队——它们几乎会同时进行,这是我不想要的。信号量在这里合理吗?或者有没有更好的方法让 NSOperations 等待其他人的异步结束?还是别的什么? 不,在这种情况下不要使用信号量。如果您有要向其中添加 AFHTTPRequestOperation 对象的操作队列,那么您应该只创建一个完成操作(您将依赖于其他操作)。或者使用调度组。顺便说一句,您说您不希望它们同时运行,如果这是您需要的,那很好,但是您会按顺序执行此操作而不是同时执行此操作会付出严重的性能损失。我一般使用 4 或 5 的 maxConcurrentOperationCount【参考方案10】:

这是一个不使用信号量的绝妙技巧:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^

    [object doSomething];
);
dispatch_sync(serialQ, ^ );

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

【讨论】:

这个答案的问题在于它没有解决OP的原始问题,即需要使用的API将completionHandler作为参数并立即返回。即使 completionHandler 尚未运行,在此答案的异步块内调用该 API 也会立即返回。然后同步块将在完成处理程序之前执行。【参考方案11】:

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

void (^nextOperationAfterLongOperationBlock)(void) = ^

;

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

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

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


【讨论】:

以上是关于如何等待异步调度的块完成?的主要内容,如果未能解决你的问题,请参考以下文章

Android:ANR 输入调度超时

Redux thunk:如何等待异步操作的完成

在使用 flutter_bloc 库调度事件之前等待一些结果

Vue/Vuex 在创建的钩子中等待异步调度

Celery异步的分布式任务调度理解

了解 dispatch_queues 和同步/异步调度