等待多个块完成

15

我有这些方法从互联网中检索一些对象信息:

- (void)downloadAppInfo:(void(^)())success
                failure:(void(^)(NSError *error))failure;
- (void)getAvailableHosts:(void(^)())success
                  failure:(void(^)(NSError *error))failure;
- (void)getAvailableServices:(void(^)())success
                     failure:(void(^)(NSError *error))failure;
- (void)getAvailableActions:(void(^)())success
                    failure:(void(^)(NSError *error))failure;

下载的内容会被存储在对象属性中,这就是为什么成功函数不返回任何内容的原因。

现在,我想要有一个像这样的方法:

- (void)syncEverything:(void(^)())success
               failure:(void(^)(NSError *error))failure;

有一个方法可以做到这一点,它调用上面所有的方法,并在每个方法执行成功或失败后才返回。

我该如何实现呢?

提示:我知道在每个方法的成功块中级联方法调用是可行的。但是当以后的实现包括更多方法时,这既不“干净”也没有帮助。

尝试:

我尝试将每个调用都在NSOperation中运行,并将这些NSOperations添加到NSOperationQueue中,然后再添加一个“完成操作”,该操作依赖于之前每个操作的状态。

这样做不起作用。因为在它们各自的成功/失败块返回之前,操作被视为已完成。

我还尝试使用dispatch_group。但我不确定我是否做对了。不幸的是,它不起作用。


2
将每个操作添加到调度组中,然后在最后发送您的最终块。 - Abizern
1
你可以查看苹果文档中的调度组相关内容:https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW25 - CarlJ
有趣。我不知道这个调度组。我会看一下,但它看起来非常有前途。 - Hasib Samad
显然没有帮助。请看我的编辑。 - Hasib Samad
@H.A.Samad 关于调度组,我在下面添加了一个答案,使用了它们。 - pkamb
5个回答

18

从其他答案的评论和博客文章 "使用Dispatch Groups等待多个Web服务" 中汲取灵感,我得出了以下答案。

此解决方案使用dispatch_group_enterdispatch_group_leave来确定每个中间任务何时运行。当所有任务都完成时,将调用最终的dispatch_group_notify块。然后,您可以调用您的完成块,知道所有中间任务都已完成。

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

    // All group blocks have now completed

    if (completion) {
        completion();
    }
});

Grand Central Dispatch - 调度组

https://developer.apple.com/documentation/dispatch/dispatchgroup

将块分组允许进行聚合同步。即使这些块在不同的队列上运行,您的应用程序也可以提交多个块并跟踪它们何时全部完成。当所有指定任务完成后才能进行进一步操作时,此行为非常有用。

Xcode代码段:

我发现自己使用调度组足够频繁,因此我将以下代码添加为Xcode代码段以便在我的代码中轻松插入。

现在我只需键入DISPATCH_SET,然后插入以下代码。 然后,您为每个异步块复制并粘贴一个enter/leave

Objective-C:

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);

dispatch_group_leave(group);

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

});

Swift:

let dispatchGroup = DispatchGroup()

dispatchGroup.enter()

dispatchGroup.leave()

dispatchGroup.notify(queue: .global()) {

}

这在你不必串行调用的情况下有效。如果你想要等待第一个调用返回后再进行第二个调用,以此类推... 这样做是不行的。 - Inn0vative1
好的,非常干净利落!不过您是否需要担心第一对dispatch_group_enter / dispatch_group_leave 和第二对之间的竞态条件呢?换句话说,如果第一个块在第二个块开始之前就结束了,那么dispatch_group_notify是否有可能触发完成操作? 如果是这样,一种解决方法是在任何“离开”调用可能被触发之前进行所有dispatch_group_enter调用。 - software evolved
@softwareevolved dispatch_group_notify 行应该放在两个 dispatch_group_enter 调用的下方,这样 notify 块就不存在,因此不能仅在第一个 enter 后调用。我认为你提出的竞争条件是有效的,但如果你将 notify 写在顶部,然后将所有的 enter/leave 对放在下面,那么这将是合法的代码,据我所见。 - pkamb
确保没有竞态条件的另一种方法是在创建组后立即进行组进入,执行各种任务(其中也包括进入/离开),然后在通知后进行离开。这大致类似于旧的保护变量所使用的保留/释放代码。 - software evolved

10

你已经接近成功了,问题很可能在于这些方法是异步的,所以你需要额外的同步步骤。只需尝试以下修复:

for(Appliance *appliance in _mutAppliances) {
  dispatch_group_async(
     group,
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       dispatch_semaphore_t sem = dispatch_semaphore_create( 0 );

       NSLog(@"Block START");

       [appliance downloadAppInfo:^{
          NSLog(@"Block SUCCESS");
            dispatch_semaphore_signal(sem);
       }
       failure:^(NSError *error){
         NSLog(@"Block FAILURE");
         dispatch_semaphore_signal(sem);
       }];

       dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

       NSLog(@"Block END");
 });

 dispatch_group_notify(
   group,
   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
     NSLog(@"FINAL block");
     success();
 });
}

运行得非常好。我最好阅读一些关于整个GCD的文档。不过,有一个问题,这是否可以使用NSOperations而不是GCD来实现? - Hasib Samad
是的,不过它会变得更加复杂一些。我现在没有时间回复,如果你真的想知道如何使用操作队列来做,也许可以尝试开一个新问题? :) - micantox
8
不建议使用 dispatch_semaphore_wait() 来阻塞线程,而是建议在同一组中使用 dispatch_group_enter() 和 dispatch_group_leave() 来 "扩展" 组到完成回调,这样一切都可以完全异步。 - das
6
我想表达的意思是,在你使用dispatch_semaphore_create()的地方应该使用dispatch_group_enter(),在你使用dispatch_semaphore_signal()的地方应该使用dispatch_group_leave(),并且移除dispatch_sempahore_wait()。如果其中一个是可行的,那么另一个也是可行的。 - das
1
使用dispatch_group_enter()和dispatch_group_leave()可以让您按顺序执行任务吗?例如,我想向服务器发送删除请求,一旦完成,我就想在本地执行删除操作。这些必须一个接一个地完成。 - NYC Tech Engineer
显示剩余2条评论

2
另一种解决方案是使用第三方库中提供的 Promise。我是 RXPromise 的作者,该库实现了 Promises/A+ 规范。但至少还有其他两个 Objective-C 实现。
Promise 表示异步方法或操作的最终结果:
-(Promise*) doSomethingAsync;

承诺是完成处理程序的完全替代品。此外,由于其明确的规范和基础设计,它具有一些非常有用的功能,使其特别容易处理相当复杂的异步问题。
首先,您需要将带有完成处理程序的异步方法包装成返回承诺的异步方法: (有意地,您的方法返回“最终结果”和“潜在错误”,以更方便的完成处理程序为目的)
例如:
- (RXPromise*) downloadAppInfo {
    RXPromise* promise = [RXPromise new];
    [self downloadAppInfoWithCompletion:^(id result, NSError *error) {
        if (error) {
            [promise rejectWithReason:error];
        } 
        else {
            [promise fulfillWithValue:result];
        }
    }];
    return promise;
}

在这里,原始的异步方法成为了 Promise 的“解析器”。Promise 可以通过指定任务的最终结果或失败的原因来实现成功(已完成)或拒绝(失败)。Promise 将持有异步操作或方法的最终结果。
请注意,包装器是一个异步方法,它立即返回处于“挂起”状态的 Promise。
最后,您可以通过使用 then 方法或属性注册成功和失败处理程序来获取最终结果。虽然一些 Promise 库略有不同,但基本上如下所示:
`promise.then( <success-handler>, <error-handler> )`

Promise/A+规范具有极简API。以上基本上就是实现Promise/A+规范所需的全部内容,通常在许多简单应用情况下已经足够了。
但有时您需要更多 - 例如OP的问题,需要“等待”一组异步方法完成,然后执行某些操作。
幸运的是,Promise是构建更复杂的辅助方法的理想基础模块,这可以相当容易地实现。
许多Promise库提供实用程序方法。因此,例如一个名为all的方法(或类似方法),它是一个异步方法,返回一个Promise并以Promise数组作为输入。当所有操作都完成或其中一个失败时,返回的Promise将被解析。可能如下所示:
首先构造一个Promise数组,并同时启动所有异步任务:
NSArray* tasks = @[
    [self downloadAppInfo],
    [self getAvailableHosts],
    [self getAvailableServices],
    [self getAvailableActions],
];

注意:这里的任务已经在运行(并且可能已经完成)!
现在,使用一个辅助方法来实现上述要求:
RXPromise* finalPromise = [RXPromise all:tasks];

获取最终结果:

finalPromise.then(^id( results){
    [self doSomethingWithAppInfo:results[0] 
                  availableHosts:results[1] 
               availableServices:results[2]  
                availableActions:results[3]];
    return nil;
},  ^id(NSError* error) {
    NSLog(@"Error %@", error); // some async task failed - log the error
});

请注意,在 all: 方法中,无论返回的承诺以何种方式得到解决,都会调用成功或失败处理程序之一。
当以下情况发生时,返回的承诺(finalPromise)将得到解决:
  1. 所有任务都成功完成。
  2. 一个任务失败。
对于情况1),最终承诺将通过一个包含每个相应异步任务结果的数组得到解决。
在情况2)下,最终承诺将通过失败的异步任务的错误得到解决。
(注:少数可用库可能存在差异)
RXPromise 库具有一些额外的功能:
- 复杂的取消功能,可以在承诺的非循环图中转发取消信号。 - 指定处理程序运行的分派队列的方法。例如,队列可以用于同步访问共享资源。
self.usersPromise = [self fetchUsers];

self.usersPromise.thenOn(dispatch_get_main_queue(), ^id(id users) {
    self.users = users;
    [self.tableView reloadData];
}, nil);

与其他方法相比,dispatch_group的解决方案存在一个问题,即它会阻塞线程。这并不完全是“异步”的。而且,如果要实现取消操作,则会变得相当复杂,甚至可能无法实现。 NSOperation的解决方案似乎是一把双刃剑。只有当你已经有了NSOperations,并且在定义依赖关系时没有需要考虑的完成处理程序时,它才可能是优雅的,否则它将变得混乱和繁琐。
到目前为止,还有另一种解决方案未被提及,那就是Reactive Cocoa。在我看来,它是一个非常棒的库,可以让你解决几乎任何复杂度的异步问题。然而,它有一个相当陡峭的学习曲线,并且可能会给你的应用程序增加很多代码。我猜,你遇到的90%的异步问题都可以用可取消的promise来解决。如果你有更复杂的问题,请看看RAC。

你为什么称它为“Promise”?对我来说,“Promise”意味着保证的东西。在编写类名称时,这是您的思考过程吗? - Pavan
1
@Pavan 它简单地遵循了 Java Script Promises/A+ specification 的约定。例如,在 Scala 中,这将是一个只读的 _Future_,它有一个相应的类 Promise 用于解决未来的问题。另请参见:Scala Futures and Promiseswiki futures and promises - CouchDeveloper

1
如果您想创建基于块的解决方案,可以尝试以下方法:
- (void)syncEverything:(void(^)())success failure:(void(^)(NSError *error))failure
{
    __block int numBlocks = 4;
    __block BOOL alreadyFailed = NO;

    void (^subSuccess)(void) = ^(){
        numBlocks-=1;
        if ( numBlocks==0 ) {
            success();
        }
    };
    void (^subFailure)(NSError*) = ^(NSError* error){
        if ( !alreadyFailed ) {
            alreadyFailed = YES;
            failure(error);
        }
    };

    [self downloadAppInfo:subSuccess failure:subFailure];
    [self getAvailableHosts:subSuccess failure:subFailure];
    [self getAvailableServices:subSuccess failure:subFailure];
    [self getAvailableActions:subSuccess failure:subFailure];
}

这有点快速而粗略,你可能需要进行块复制。如果有多个方法失败,你只会得到一个总体失败。


而 dispatch_groups 做得更好。 - Abizern
1
@Abizern,您能否发布一些代码,关于如何使用dispatch_groups进行操作?我似乎无法弄清楚(请参见我的上面的编辑)。 - Hasib Samad
@micantox 是的,我也有点想知道,两个线程是否可以同时执行同一个块,或者块对象是否是线程安全的? - fresidue

0

这是我的解决方案,没有使用任何dispatch_group。

+(void)doStuffWithCompletion:(void (^)(void))completion{
    __block NSInteger stuffRemaining = 3;

    void (^dataCompletionBlock)(void) = ^void(void) {
        stuffRemaining--;

        if (!stuffRemaining) {
            completion();
        }
    };

    for (NSInteger i = stuffRemaining-1; i > 0; i--) {
        [self doOtherStuffWithParams:nil completion:^() {
            dataCompletionBlock();
        }];
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接