【问题标题】:How to wait for method that has completion block (all on main thread)?如何等待具有完成块的方法(全部在主线程上)?
【发布时间】:2013-07-29 09:17:59
【问题描述】:

我有以下(伪)代码:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
            }];

            // B. Need to wait here until A is completed.
        }
    }];

    // C. Need to wait here until all iterations above have finished.
    STAssertTrue(...);
}

这段代码在主线程上运行,完成块A也在主线程上。

  • 如何在 B 处等待 A 完成?
  • 随后如何在 C 处等待外部完成块完成?

【问题讨论】:

  • // B.这里需要等到A完成应该在block的末尾
  • @AndreyChernukha 不,因为someObject 在其他线程上做事,并且只在主线程上调用完成块。 B 在块内时,所有循环迭代都将执行,而无需先等待前一个 A 完成。
  • 那你就不需要块了
  • @AndreyChernukha 你是什么意思?如果您的意思是我不需要阻止 = 等待,那么不需要。在 A 的所有迭代完成之后,我需要在 C 处做点什么。
  • 我的意思是:1)lookupName 2)A 3)B 所有随后都没有将 A 放入块中

标签: ios objective-c


【解决方案1】:

如果你的完成块也在主线程上被调用,可能很难做到,因为在完成块执行之前,你的方法需要返回。您应该将异步方法的实现更改为:

  1. 保持同步。
  2. 使用其他线程/队列完成。然后你可以使用 Dispatch Semaphores 等待。您使用值 0 初始化一个信号量,然后在主线程上调用 wait 并在完成时调用 signal

无论如何,在 GUI 应用程序中 阻塞主线程是一个非常糟糕的主意,但这不是您问题的一部分。在测试、命令行工具或其他特殊情况下可能需要阻塞主线程。在这种情况下,请进一步阅读:


如何在主线程上等待主线程回调

有办法做到这一点,但可能会产生意想不到的后果。 谨慎行事!

主线程很特别。它运行+[NSRunLoop mainRunLoop],它还处理+[NSOperationQueue mainQueue]dispatch_get_main_queue()。分派到这些队列的所有操作或块都将在主运行循环中执行。这意味着,这些方法可以采用 any 方法来调度完成块,这应该适用于所有这些情况。这里是:

__block BOOL isRunLoopNested = NO;
__block BOOL isOperationCompleted = NO;
NSLog(@"Start");
[self performOperationWithCompletionOnMainQueue:^{
    NSLog(@"Completed!");
    isOperationCompleted = YES;
    if (isRunLoopNested) {
        CFRunLoopStop(CFRunLoopGetCurrent()); // CFRunLoopRun() returns
    }
}];
if ( ! isOperationCompleted) {
    isRunLoopNested = YES;
    NSLog(@"Waiting...");
    CFRunLoopRun(); // Magic!
    isRunLoopNested = NO;
}
NSLog(@"Continue");

这两个布尔值是为了在块立即同步完成的情况下确保一致性。

如果-performOperationWithCompletionOnMainQueue:异步,则输出为:

开始
等待...
完成!
继续

如果方法是同步,则输出为:

开始
完成!
继续

什么是魔法?调用 CFRunLoopRun() 不会立即返回,但只有在调用 CFRunLoopStop() 时才会返回。此代码在 Main RunLoop 上,因此运行 Main RunLoop 再次将恢复所有计划块、计时器、套接字等的执行。

警告:可能的问题是,所有其他计划的计时器和块将同时执行。此外,如果从不调用完成块,您的代码将永远不会到达Continue 日志。

您可以将此逻辑包装在一个对象中,这样可以更容易地重复使用此模式:

@interface MYRunLoopSemaphore : NSObject

- (BOOL)wait;
- (BOOL)signal;

@end

所以代码会简化成这样:

MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new];
[self performOperationWithCompletionOnMainQueue:^{
    [semaphore signal];
}];
[semaphore wait];

【讨论】:

  • 等待完成是完成块的目的。因此,在它自己的完成块中调用 lookupName 以递归地遍历每个数组元素应该可以正常工作。
  • 不,你错了。这不应该被接受。 @johnyu 完全正确。
  • @LeonardPauli 如果问题询问 “等待” 的含义,即“阻止执行直到发生某些事情”,这是不可能的。
  • iMartin 可能,但我希望情况并非如此,正如您所指出的那样。正如@johnyu 和我所写的那样,提问者所写的相同效果可以用块和函数存档。对不起,如果我有点生气......
  • OP 的问题似乎来自单元测试,在这种情况下这实际上是可能的(如果它也是实际代码也是可能的,只是由于阻塞了主线程而不可取)。关键是触发运行循环并等待您在完成处理程序中设置的标志。有关详细信息,请参阅此答案:stackoverflow.com/questions/7817605/…
【解决方案2】:

我认为 Mike Ash (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.html 完全可以回答“等待几个线程完成,然后在所有线程完成后做某事”。好处是您甚至可以使用调度组同步或异步等待。

从 Mike Ash 的博客中复制和修改的一个简短示例:

    dispatch_group_t group = dispatch_group_create();

    for(int i = 0; i < 100; i++)
    {
        dispatch_group_enter(group);
        DoAsyncWorkWithCompletionBlock(^{
            // Async work has been completed, this must be executed on a different thread than the main thread

            dispatch_group_leave(group);
        });
    }

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

或者,您可以异步等待并在所有块完成时执行操作,而不是 dispatch_group_wait:

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    UpdateUI();
});

【讨论】:

  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
  • @laalto,谢谢我已经更新了我的答案以包含一个代码示例
  • 改变了我的答案,这个解决方案显然只在完成块不在主线程上执行时才有效
【解决方案3】:
int i = 0;
//the below code goes instead of for loop
NSString *name = [names objectAtIndex:i];

[someObject lookupName:name completion:^(NSString* urlString)
{
    // A. Something that takes a few seconds to complete.
    // B.
    i+= 1;
    [self doSomethingWithObjectInArray:names atIndex:i];


}];




/* add this method to your class */
-(void)doSomethingWithObjectInArray:(NSArray*)names atIndex:(int)i {
    if (i == names.count) {
        // C.
    }
    else {
        NSString *nextName = [names objectAtIndex:i];
        [someObject lookupName:nextName completion:^(NSString* urlString)
        {
            // A. Something that takes a few seconds to complete.
            // B.
            [self doSomethingWithObjectInArray:names atIndex:i+1];
        }];
    }
}

我只是在这里输入代码,所以某些方法名称可能拼写错误。

【讨论】:

  • 不起作用,因为循环将触发所有四个 lookupName 调用,这需要一些时间。这里根本不需要等待。
  • 不起作用,因为retrievelookupName 都会立即返回。
  • 我的回答中没有“检索”。 O.o
  • 如果没有retrieve,那么你在我的代码中遗漏了一些东西;它不起作用的另一个原因。我不能用块来改变这些方法,所以你不能改变代码而忽略它。
  • 我在我的代码中有评论说它的一部分用于您发布的内容,替换了 for 循环。那是在检索方法中。如果retrieve 和lookupName 立即返回,那只是因为您实现了这些方法。
【解决方案4】:

我目前正在开发一个库(RXPromise,其源代码在 GitHub 上),它使许多复杂的异步模式很容易实现。

以下方法利用类RXPromise 并生成100% 异步的代码——这意味着绝对没有阻塞。 “等待”将通过异步任务完成或取消时调用的处理程序来完成。

它还使用了NSArray 的类别,这不是库的一部分 - 但可以使用 RXPromise 库轻松实现。

例如,您的代码可能如下所示:

- (RXPromise*)asyncTestAbc
{
    return [someThing retrieve:@"foo"]
    .then(^id(id unused /*names?*/) {
        // retrieve:@"foo" finished with success, now execute this on private queue:
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        return [names rx_serialForEach:^RXPromise* (id name) { /* return eventual result when array finished */
            return [someObject lookupName:name] /* return eventual result of lookup's completion handler */
            .thenOn(mainQueue, ^id(id result) {
                assert(<we are on main thread>);
                // A. Do something after a lookupName:name completes a few seconds later
                return nil;
            }, nil /*might be implemented to detect a cancellation and "backward" it to the lookup task */);
        }]
    },nil);
}

为了测试最终结果:

[self asyncTestAbc]
.thenOn(mainQueue, ^id(id result) {
    // C. all `[someObject lookupName:name]` and all the completion handlers for
    // lookupName,  and `[someThing retrieve:@"foo"]` have finished.
    assert(<we are on main thread>);
    STAssertTrue(...);
}, id(NSError* error) {
    assert(<we are on main thread>);
    STFail(@"ERROR: %@", error);
});

asyncTestABC 方法将完全按照您的描述进行 - 除了它是异步。出于测试目的,您可以等到它完成:

  [[self asyncTestAbc].thenOn(...) wait];

但是,您不能在主线程上等待,否则您会遇到死锁,因为asyncTestAbc 也会在主线程上调用完成处理程序。


如果您觉得这很有用,请索取更详细的解释!


注意:RXPromise 库仍在“进行中”。它可以帮助每个人处理复杂的异步模式。上面的代码使用了一个当前未提交给 GitHub 上的 master 的功能:Property thenOn,其中可以指定一个队列,其中将执行处理程序。目前只有属性then 省略了处理程序应运行的参数队列。除非另有说明,否则所有处理程序都在共享专用队列上运行。欢迎提出建议!

【讨论】:

  • 我也强烈推荐 RXPromise 作为简化异步调用的库——我们在所有项目中都使用它
  • @angryTurtle 感谢您的热情支持。如今,RXPromise 处于稳定版本 v1.0.2 并已用于 1600 多个项目。
【解决方案5】:

阻塞主线程通常是一种不好的方法,它只会让你的应用程序无响应,所以为什么不做这样的事情呢?

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Insert code for adding loading animation

    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Insert code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }


    NSString *name = [names objectAtIndex:namesIndex];
    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

我刚刚使用 [UIView 动画...] 使示例功能齐全。只需复制并粘贴到您的 viewcontroller.m 并调用 [self setup];当然,你应该用你的代码替换它。

或者如果你愿意:

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Code for adding loading animation

    [someThing retrieve:@"foo" completion:^ {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }

    NSString *name = [names objectAtIndex:namesIndex];
    [someObject lookupName:name completion:^(NSString* urlString) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

解释:

  1. 通过调用 [self setup] 开始一切;
  2. 当 someThing 检索到“foo”时将调用一个块,换句话说,它将等待直到 someThing 检索到“foo”(并且不会阻塞主线程)
  3. 当块被执行时,alterNames 被调用
  4. 如果“names”中的所有项都已循环通过,则“循环”将停止,可以执行 C。
  5. 另外,查找名称,完成后,用它做一些事情 (A),因为它发生在主线程上(你没有说别的),你可以做B 也在那里。
  6. 所以,当 A 和 B 完成后,跳回 3

看到了吗?

祝你的项目好运!

【讨论】:

  • 奇怪的是,您使用 UIView 确实会延迟;还有很多更好的方法。延迟不起作用,因为没有定义持续时间;等待固定的时间是很糟糕的。
  • 不起作用,因为retrievelookupName 都会立即返回。
  • 你试过了吗!?它对我来说非常好,我看不出有什么问题......当然,如果 A 也在另一个线程上做某事,那么你将不得不再次重复我做的事情。基本上,完成后重新调用该方法而不是循环。这是有效的,接受的答案是错误的。我一直在用这个。
  • 查看我的新编辑以获得解释。它确实有效!是的,这些方法会立即返回,但这就是完成块的重点!!他们不会立即执行!
  • 请尝试,不要说它真的行不通。我的第一个答案有效(好吧,它有一个拼写错误,但仍然存在)。啊哈……
【解决方案6】:

上面有很多很好的通用答案 - 但看起来您正在尝试为使用完成块的方法编写单元测试。在调用块之前,您不知道测试是否通过,这是异步发生的。

在我当前的项目中,我使用SenTestingKitAsync 来执行此操作。它扩展了 OCTest,以便在运行所有测试之后,它会执行主运行循环中等待的任何内容并评估这些断言。所以你的测试可能看起来像:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        STSuccess();
    }];

    STFailAfter(500, @"block should have been called");
}

我还建议在两个单独的测试中测试 someThingsomeObject,但这与您正在测试的内容的异步性质无关。

【讨论】:

  • 我的someThingsomeObject 都需要我才能开始测试;他们从外部互联网资源中获取信息。
  • 感谢您指向 SenTestingKitAsync;我不知道。
  • 没问题;忘记了我在其中找到的博客文章。关于someThingsomeObject,您可能会考虑使用模拟来分隔两者之间的测试。但这又进入了另一个讨论……
【解决方案7】:
 Move B and C to two methods.

int flagForC = 0, flagForB = 0;
     [someThing retrieve:@"foo" completion:^
    {
        flagForC++;
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
               flagForB++;

               if (flagForB == [names Count])
               {
                   flagForB = 0;
                   //call B
                    if (flagForC == thresholdCount)
                    {
                          flagForC = 0;
                         //Call C 
                    }
               }
            }];


        }
    }];

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多