等待两个异步块执行完毕再开始另一个块。

202

使用GCD时,我们希望在继续执行下一步之前等待两个异步块被执行并完成。最佳方法是什么?

我们尝试了以下方式,但似乎不起作用:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
});


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
});

// wait until both the block1 and block2 are done before start block3
// how to do that?

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
});

查看我的答案,其中提供了 Swift 5 的六种不同解决问题的方法。 - Imanou Petit
10个回答

320

使用调度组:请参见此处的示例,这是苹果的iOS开发者库中《并发编程指南》中“调度队列”章节中的“等待排队任务组”的部分。

您的示例可能类似于以下内容:

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
    NSLog(@"Block1");
    [NSThread sleepForTimeInterval:5.0];
    NSLog(@"Block1 End");
});


dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
    NSLog(@"Block2");
    [NSThread sleepForTimeInterval:8.0];
    NSLog(@"Block2 End");
});

dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
    NSLog(@"Block3");
});

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group);

并且可以生成如下输出:

2012-08-11 16:10:18.049 Dispatch[11858:1e03] Block1
2012-08-11 16:10:18.052 Dispatch[11858:1d03] Block2
2012-08-11 16:10:23.051 Dispatch[11858:1e03] Block1 End
2012-08-11 16:10:26.053 Dispatch[11858:1d03] Block2 End
2012-08-11 16:10:26.054 Dispatch[11858:1d03] Block3

3
好的。一旦异步任务/块与组相关联,它们会被顺序执行还是并发执行?我的意思是,假设现在块1和块2与一个组相关联,那么在块1完成之前,块2是否会等待它才能开始执行? - tom
9
由你决定。dispatch_group_asyncdispatch_async类似,只是添加了一个组参数。所以如果你对block1和block2使用了不同的队列,或者在同一并发队列上安排它们,它们可以并行运行;如果你将它们安排在同一串行队列上,它们将串行运行。这与没有使用组调度块没有什么不同。 - Jörn Eyrich
1
这是否也适用于执行 Web 服务的 POST 请求? - SleepNot
你是否注意到时间与你所设置的睡眠时间不相等?为什么会这样呢? - Damon Yuan
2
在ARC中,只需删除dispatch_release(group)即可。 - loretoparisi
显示剩余3条评论

285

在Jörn Eyrich的回答上进行扩展(如果您点赞了我的回答,也请点赞他的回答),如果您无法控制块的dispatch_async调用,就像异步完成块的情况一样,您可以直接使用GCD组并使用dispatch_group_enterdispatch_group_leave

在此示例中,我们假装computeInBackground是我们无法更改的内容(想象它是委托回调,NSURLConnection completionHandler或其他内容),因此我们无法访问调度调用。

// create a group
dispatch_group_t group = dispatch_group_create();

// pair a dispatch_group_enter for each dispatch_group_leave
dispatch_group_enter(group);     // pair 1 enter
[self computeInBackground:1 completion:^{
    NSLog(@"1 done");
    dispatch_group_leave(group); // pair 1 leave
}];

// again... (and again...)
dispatch_group_enter(group);     // pair 2 enter
[self computeInBackground:2 completion:^{
    NSLog(@"2 done");
    dispatch_group_leave(group); // pair 2 leave
}];

// Next, setup the code to execute after all the paired enter/leave calls.
//
// Option 1: Get a notification on a block that will be scheduled on the specified queue:
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"finally!");
});

// Option 2: Block an wait for the calls to complete in code already running
// (as cbartel points out, be careful with running this on the main/UI queue!):
//
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // blocks current thread
// NSLog(@"finally!");

在这个例子中,computeInBackground:completion:的实现如下:

- (void)computeInBackground:(int)no completion:(void (^)(void))block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%d starting", no);
        sleep(no*2);
        block();
    });
}

输出(带有运行时间戳):

12:57:02.574  2 starting
12:57:02.574  1 starting
12:57:04.590  1 done
12:57:06.590  2 done
12:57:06.591  finally!

1
@ɲeuroburɳ 上述代码在主线程上等待。我认为这会阻塞主线程并导致UI无响应,直到整个组完成。我建议将等待移动到后台线程。例如,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)。 - cbartel
3
@cbartel,好发现!我已经更新了示例代码以反映您的评论。许多时候,你需要回调在主队列上--在那种情况下,dispatch_queue_notify可能更好(除非阻塞时间保证很短)。 - ɲeuroburɳ
我应该在哪里释放这个组(即dispatch_release(group))?我不确定在dispatch_group_notify中释放是否安全。但由于这是在组完成后运行的代码,所以我不确定何时释放。 - GingerBreadMane
如果您正在使用ARC,则无需调用dispatch_release:https://dev59.com/questions/dGoy5IYBdhLWcg3wWcmg - ɲeuroburɳ
如果没有使用ARC,则“在设置通知块后释放组是有效的”。 https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/dispatch_group_notify.3.html - ɲeuroburɳ
3
很棒的文章,进一步解释了使用Dispatch Group来等待多个Web服务的方法。链接为http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/ - Rizon

126

在Swift 5.1中,Grand Central Dispatch提供了许多解决问题的方法。根据您的需求,您可以选择以下Playground片段中展示的七种模式之一。


#1. 使用DispatchGroupDispatchGroupnotify(qos:flags:queue:execute:)DispatchQueueasync(group:qos:flags:execute:)

苹果开发者并发编程指南介绍了DispatchGroup

调度组是一种阻止线程直到一个或多个任务执行完成的方法。您可以在无法取得进展直到所有指定的任务完成的地方使用此行为。例如,在分配几个任务来计算某些数据后,您可以使用组来等待这些任务,然后在它们完成时处理结果。
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

queue.async(group: group) {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async(group: group) {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

group.notify(queue: queue) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

#2. 使用 DispatchGroupDispatchGroupwait()DispatchGroupenter()DispatchGroupleave()
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

group.enter()
queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    group.leave()
}

group.enter()
queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    group.leave()
}

queue.async {
    group.wait()
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

请注意,您还可以混合使用DispatchGroup wait()DispatchQueue async(group:qos:flags:execute:),或者混合使用DispatchGroup enter()DispatchGroup leave()DispatchGroup notify(qos:flags:queue:execute:)

#3. 使用 DispatchWorkItemFlags barrierDispatchQueueasync(group:qos:flags:execute:)

Raywenderlich.com上的Swift 4 Grand Central Dispatch教程:第1/2部分文章给出了barriers的定义:

调度屏障是一组函数,作为并发队列工作时的串行式瓶颈。当您向调度队列提交DispatchWorkItem时,可以设置标志以指示它应该是在特定时间内执行的指定队列中唯一执行的项。这意味着,在调度屏障之前提交到队列的所有项必须完成,才能执行DispatchWorkItem
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

queue.async(flags: .barrier) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

#4. 使用 DispatchWorkItemDispatch​Work​Item​FlagsbarrierDispatchQueueasync(execute:)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let dispatchWorkItem = DispatchWorkItem(qos: .default, flags: .barrier) {
    print("#3 finished")
}

queue.async(execute: dispatchWorkItem)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

#5. 使用DispatchSemaphoreDispatchSemaphorewait()DispatchSemaphoresignal()

Soroush Khanlou在The GCD Handbook博客文章中写道:

使用信号量,我们可以阻止一个线程任意地等待,直到来自另一个线程的信号被发送。像GCD的其余部分一样,信号量是线程安全的,并且它们可以从任何位置触发。当您需要将异步API变为同步API但无法修改它时,可以使用信号量。

苹果开发者API参考还为DispatchSemaphore init(value:​)初始化程序提供了以下讨论:

将值设为零对于两个线程需要协调特定事件的完成时非常有用。将值设置为大于零的值对于管理资源有限的池很有用,其中池的大小等于该值。

用法:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 0)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    semaphore.signal()
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    semaphore.wait()    
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

#6. 使用OperationQueueOperationaddDependency(_:)

关于Operation​Queue,苹果开发者API参考文档中这样描述:

操作队列使用libdispatch库(也称为Grand Central Dispatch)来启动它们的操作执行。

用法:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let blockThree = BlockOperation {
    print("#3 finished")
}

blockThree.addDependency(blockOne)
blockThree.addDependency(blockTwo)

operationQueue.addOperations([blockThree, blockTwo, blockOne], waitUntilFinished: false)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

#7. 使用 OperationQueueOperationQueueaddBarrierBlock(_:)(需要 iOS 13)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

operationQueue.addOperations([blockTwo, blockOne], waitUntilFinished: false)
operationQueue.addBarrierBlock {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

有没有一种解决异步调用的方法,而不需要为每个调用使用group.enter()和group.leave()(也不需要使用信号量)?比如我需要等待一个异步向服务器的请求,然后再等待第二个异步请求,以此类推。 我读了这篇文章https://www.avanderlee.com/swift/asynchronous-operations/,但是与BlockOperation相比,我并没有看到它的简单用法。 - Woof

57

另一个GCD的替代方案是屏障:

dispatch_queue_t queue = dispatch_queue_create("com.company.app.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{ 
    NSLog(@"start one!\n");  
    sleep(4);  
    NSLog(@"end one!\n");
});

dispatch_async(queue, ^{  
    NSLog(@"start two!\n");  
    sleep(2);  
    NSLog(@"end two!\n"); 
});

dispatch_barrier_async(queue, ^{  
    NSLog(@"Hi, I'm the final block!\n");  
});

只需创建一个并发队列,分派两个块,然后使用障碍调度最终的块,这将使其等待其他两个块完成。


如果我没有使用sleep(4)会有什么问题吗? - Himanth
不,当然没有问题。实际上,你几乎永远不想使用 sleep()!我只是为了教学目的而添加了这些 sleep() 调用,以使块运行足够长的时间,以便您可以看到它们并发运行。在这个简单的例子中,在没有 sleep() 的情况下,这两个块可能运行得非常快,以至于分派的块可能在你有机会经验性地观察并发执行之前就开始和结束了。但是在你自己的代码中不要使用 sleep() - Rob

40

我知道你在问关于GCD的事情,但如果你想的话,NSOperationQueue也可以非常优雅地处理这种任务,例如:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 3");
}];

NSOperation *operation;

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 1");
    sleep(7);
    NSLog(@"Finishing 1");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 2");
    sleep(5);
    NSLog(@"Finishing 2");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

[queue addOperation:completionOperation];

3
当您的NSBlockOperation内部代码是同步的时,这很好。但如果不是,并且您希望在异步操作完成时触发完成,该怎么办? - Greg Maletic
3
在这种情况下,我创建一个并发的NSOperation子类,并在异步过程完成时设置isFinished。然后依赖关系就可以正常工作了。 - Rob
@GregMaletic 请参考 https://dev59.com/VnbZa4cB1Zd3GeqPBhXy#18431755 和 https://dev59.com/tnPYa4cB1Zd3GeqPpv49#17427268 获取示例。 - Rob
1
@GregMaletic 是的,你也可以使用那个方法(只要 dispatch_semaphore_wait 不在主队列中执行,并且你的信号和等待是平衡的)。只要不阻塞主队列,使用信号量方法是可以的,如果你不需要操作的灵活性(例如取消操作的能力、控制并发度的能力等)。 - Rob
1
@Reza.Ab - 如果需要在任务二开始之前完成任务一,请在这些任务之间添加依赖关系。或者,如果队列始终只执行一个任务,请通过将“maxConcurrentOperationCount”设置为“1”来使其成为串行队列。您还可以设置操作的优先级,包括“qualityOfService”和“queuePriority”,但是与依赖关系和/或队列并发度相比,这些对任务优先级的影响要微妙得多。 - Rob
显示剩余7条评论

4

以上的答案都很棒,但是它们都漏掉了一件事情。当你使用dispatch_group_enter/dispatch_group_leave时,group在进入的线程中执行任务(blocks)。

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      dispatch_async(demoQueue, ^{
        dispatch_group_t demoGroup = dispatch_group_create();
        for(int i = 0; i < 10; i++) {
          dispatch_group_enter(demoGroup);
          [self testMethod:i
                     block:^{
                       dispatch_group_leave(demoGroup);
                     }];
        }

        dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
          NSLog(@"All group tasks are done!");
        });
      });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

这个代码在创建的并发队列demoQueue中运行。如果我没有创建任何队列,它将在主线程中运行。

- (IBAction)buttonAction:(id)sender {
    dispatch_group_t demoGroup = dispatch_group_create();
    for(int i = 0; i < 10; i++) {
      dispatch_group_enter(demoGroup);
      [self testMethod:i
                 block:^{
                   dispatch_group_leave(demoGroup);
                 }];
    }

    dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
      NSLog(@"All group tasks are done!");
    });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

还有第三种方式可以使任务在另一个线程中执行:

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      //  dispatch_async(demoQueue, ^{
      __weak ViewController* weakSelf = self;
      dispatch_group_t demoGroup = dispatch_group_create();
      for(int i = 0; i < 10; i++) {
        dispatch_group_enter(demoGroup);
        dispatch_async(demoQueue, ^{
          [weakSelf testMethod:i
                         block:^{
                           dispatch_group_leave(demoGroup);
                         }];
        });
      }

      dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
        NSLog(@"All group tasks are done!");
      });
      //  });
    }

当然,如上所述,你可以使用dispatch_group_async来获得你想要的结果。


3

第一个答案基本上是正确的,但如果你想要最简单的方法来实现所需的结果,以下是一个独立的代码示例,演示如何使用信号量来完成它(这也是调度组在幕后工作的方式,供您参考):

#include <dispatch/dispatch.h>
#include <stdio.h>

main()
{
        dispatch_queue_t myQ = dispatch_queue_create("my.conQ", DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t mySem = dispatch_semaphore_create(0);

        dispatch_async(myQ, ^{ printf("Hi I'm block one!\n"); sleep(2); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ printf("Hi I'm block two!\n"); sleep(4); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER); printf("Hi, I'm the final block!\n"); });
        dispatch_main();
}

7
两个观察点:1. 你缺少了一个dispatch_semaphore_wait。你有两个信号,所以你需要两个等待。目前,你的“完成”块将在第一个块发出信号后立即开始,但在另一个块完成之前;2. 鉴于这是一个iOS问题,我不建议使用dispatch_main - Rob
1
我同意Rob的观点。这不是一个有效的解决方案。dispatch_semaphore_wait方法将在任一dispatch_semaphore_signal方法被调用时解除阻塞。这可能会导致它看起来像是有效的解决方案,因为“one”和“two”的printf立即发生,而“finally”的printf在等待之后发生——因此在block one睡眠2秒钟后。如果你把printf放在sleep调用之后,你会得到“one”的输出,然后2秒钟后得到“finally”的输出,再过2秒钟得到“two”的输出。 - ɲeuroburɳ

0

Swift 中被接受的答案:

let group = DispatchGroup()

group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block1
    print("Block1")
    Thread.sleep(forTimeInterval: 5.0)
    print("Block1 End")
})


group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block2
    print("Block2")
    Thread.sleep(forTimeInterval: 8.0)
    print("Block2 End")
})

dispatch_group_notify(group, DispatchQueue.global(qos: .default), {
    // block3
    print("Block3")
})

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group)

0

Swift 4.2 示例:

let group = DispatchGroup.group(count: 2)
group.notify(queue: DispatchQueue.main) {
     self.renderingLine = false
     // all groups are done
}
DispatchQueue.main.async {
    self.renderTargetNode(floorPosition: targetPosition, animated: closedContour) {
        group.leave()
        // first done
    }
    self.renderCenterLine(position: targetPosition, animated: closedContour) {
        group.leave()
        // second done
    }
 }

group.leave() 导致了崩溃。 - Ben

-4

不是说其他答案在某些情况下不好,但这是我总是从Google中使用的一个片段:

- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {


    if (signInDoneSel) {
        [self performSelector:signInDoneSel];
    }

}

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