20

我有以下(伪)代码:

- (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 处等待外部完成块完成?
4

7 回答 7

23

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

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

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


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

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

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

__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];
于 2013-07-29T09:59:06.270 回答
3

我认为 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();
});
于 2013-10-29T07:45:14.540 回答
2
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];
        }];
    }
}

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

于 2013-07-29T09:26:59.957 回答
2

我目前正在开发一个库(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 的功能:thenOn可以指定队列的属性,其中将执行处理程序。目前只有一个属性then省略了处理程序应该运行的参数队列。除非另有说明,否则所有处理程序都在共享专用队列上运行。欢迎提出建议!

于 2013-07-30T16:34:53.957 回答
1

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

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" 中的所有项目都已循环通过,则 "looping" 将停止并且可以执行 C。
  5. 否则,查找名称,当它完成后,用它做一些事情(A),因为它发生在主线程上(你没有说别的),你也可以在那里做 B。
  6. 所以,当 A 和 B 完成时,跳回 3

看?

祝你的项目好运!

于 2013-07-29T09:55:19.140 回答
1

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

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

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

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

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

于 2013-07-30T16:48:18.280 回答
0
 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 
                    }
               }
            }];


        }
    }];
于 2013-07-29T10:00:33.273 回答