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


179

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

Answers:


302

尝试使用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:确定该操作实际上没有足够的时间值得使用线程并改为同步运行,此操作也应正确执行。


61
该代码对我不起作用。我的STAssert将永远不会执行。我不得不更换dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
那可能是因为您的完成块被调度到了主队列?队列被阻塞,等待信号量,因此从不执行该块。见这个问题有关的主要队列调度无阻塞。
zoul 2012年

3
我遵循@Zoul&nicktmro的建议。但它看起来将陷入僵局。测试用例'-[BlockTestTest testAsync]'已启动。但永无止境
NSCry 2012年

3
您是否需要在ARC下释放信号量?
彼得·沃博

14
这正是我想要的。谢谢!@PeterWarbo不,你不会。ARC的使用消除了执行dispatch_release()的需要
Hulvej 2013年

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;而且,在这些测试套件中,我们必须确保直到异步调用完成后测试才能完成)。这是少数情况下可能需要使用信号量技术来阻塞主线程的情况之一。

因此,我对这个最初的问题的作者表示歉意,对于谁来说信号灯技术是健全的,我向所有看到这种信号灯技术并考虑在其代码中应用它作为处理异步的通用方法的所有新开发人员写此警告。方法:警告:十分之九,信号量技术是 不是遇到异步操作时最好的方法。相反,您应该熟悉完成阻止/关闭模式,委托协议模式和通知。这些通常是处理异步任务的更好方法,而不是使用信号量使它们同步运行。通常,将异步任务设计为异步行为是有充分理由的,因此请使用正确的异步模式,而不是尝试使其同步行为。


1
我认为这应该是现在可以接受的答案。以下是文档:developer.apple.com/library/prerelease/ios/documentation/…–
hris.to

我对此有疑问。我有一些异步代码,它们执行大约十二个AFNetworking下载调用以下载单个文档。我想安排在上进行下载NSOperationQueue。除非我使用信号量之类的文件,否则文档下载NSOperation将立即全部完成,并且不会出现真正的下载排队–它们几乎同时进行,这是我不希望的。这里的信号量合理吗?还是有更好的方法让NSOperations等待其他对象的异步结束?或者是其他东西?
Benjohn 2015年

不,在这种情况下不要使用信号灯。如果您有要向其添加AFHTTPRequestOperation对象的操作队列,那么您应该只创建一个完成操作(您将依赖于其他操作)。或使用调度组。顺便说一句,您说您不希望它们同时运行,如果这是您需要的话,这很好,但是您要为此付出巨大的性能损失,而不是同时执行。我一般用maxConcurrentOperationCount4或5的
罗布

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

24

通常不使用这些答案中的任何一个,它们通常不会扩展 (当然,到处都是例外)

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

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

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

最初的工作要多一些,但从长远来看,它将减少可怕的比赛条件和电池谋杀投票的数量。

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


1
这也是一个重要的警告,因为还有obj-C的设计模式和可测试性
BootMaker '16

8

这是一个不使用信号灯的妙招:

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

您要做的是等待dispatch_sync与空块一起使用,以同步等待串行调度队列,直到A同步块完成。


这个答案的问题是,它没有解决OP的原始问题,即需要使用的API以completionHandler作为参数并立即返回。即使completionHandler尚未运行,在此答案的异步块内调用该API也会立即返回。然后,同步块将在completionHandler之前执行。
BTRUE

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

2

还有SenTestingKitAsync,可以让您编写如下代码:

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

(有关详细信息,请参阅objc.io文章。)由于Xcode 6上有一个AsynchronousTesting类别XCTest,您可以编写这样的代码:

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

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

1
上面的代码中有错误。从NSCondition 文档-waitUntilDate:“您必须在调用此方法之前锁定接收器”。所以-unlock应该在之后-waitUntilDate:
Patrick

这不能扩展到使用多个线程或运行队列的任何对象。

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

这为我做到了。


3
好吧,虽然它会导致较高的CPU使用率
凯文

4
@kevin是,这是贫民窟民意测验,它将杀死电池。

@Barry,它如何消耗更多的电池。请指导。
pkc456'9

@ pkc456浏览一本计算机科学书籍,了解轮询和异步通知的工作方式之间的差异。祝好运。

2
四年半后,凭借我的知识和经验,我不推荐我的答案。

0

有时,超时循环也​​很有用。您可以等到从异步回调方法中收到一些信号(可能是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];
}

1
同样的问题:电池寿命失败。

1
@Barry即使您看了代码也不确定。在TIMEOUT_SECONDS时间段内,如果异步调用不响应,则会中断循环。那是打破僵局的手段。此代码完美地工作而不会耗尽电池。
Khulja Sim Sim

0

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

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

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
}

-1

我必须等到加载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

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.