等到两个异步块被执行后再开始另一个块

Posted

技术标签:

【中文标题】等到两个异步块被执行后再开始另一个块【英文标题】:Waiting until two async blocks are executed before starting another block 【发布时间】:2012-08-08 05:25:16 【问题描述】:

在使用 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
);

【问题讨论】:

请参阅my answer 了解 Swift 5,它提供多达六种不同的方法来解决您的问题。 【参考方案1】:

使用调度组:参见here 示例,Apple 的 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

【讨论】:

酷。异步任务/块一旦与组关联,将按顺序执行还是同时执行?我的意思是,假设现在 block1 和 block2 与一个组相关联,block2 会等到 block1 完成后再开始执行吗? 这取决于你。 dispatch_group_async 就像dispatch_async 一样,添加了组参数。所以如果你对block1和block2使用不同的队列或者将它们调度在同一个并发队列上,它们可以并发运行;如果您将它们安排在同一个串行队列上,它们将串行运行。这与没有组的调度块没有什么不同。 这是否也适用于执行 web service post? 您是否注意到时间不等于您块中设置的睡眠时间?为什么会这样? 在 ARC 中只需删除 dispatch_release(group);【参考方案2】:

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

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

【讨论】:

两个观察结果:1.您缺少dispatch_semaphore_wait。你有两个信号,所以你需要两次等待。照原样,您的“完成”块将在第一个块发出信号量时立即开始,但在另一个块完成之前; 2. 鉴于这是一个 iOS 问题,我不鼓励使用 dispatch_main 我同意 Rob 的观点。这不是一个有效的解决方案。只要调用了dispatch_semaphore_signal 方法中的任何一个,dispatch_semaphore_wait 就会解除阻塞。这似乎起作用的原因是块“一”和“二”的printf 立即发生,而“最终”的printf 发生在等待之后 - 因此在块一睡了 2秒。如果将 printf 放在 sleep 调用之后,您将获得 'one' 的输出,然后 2 秒后获得 'finally' 的输出,然后 2 秒后获得 'two' 的输出。【参考方案3】:

我知道你问过 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];

【讨论】:

当您的 NSBlockOperation 内部的代码是同步的时,这很好。但如果不是,并且您想在异步操作完成时触发完成怎么办? @GregMaletic 在这种情况下,我创建了一个并发的NSOperation 子类,并在异步过程完成时设置isFinished。然后依赖项工作正常。 @GregMaletic 参见***.com/questions/18429011/… 和***.com/questions/17426855/… 示例。 @GregMaletic 是的,你也可以使用它(只要dispatch_semaphore_wait 不在主队列中发生并且你的信号和等待是平衡的)。只要您不阻塞主队列,信号量方法就可以了,如果您不需要操作的灵活性(例如具有取消它们的能力、控制并发度的能力等)。 @Reza.Ab - 如果您需要在任务二开始之前完成任务一,请在这些任务之间添加依赖关系。或者,如果队列一次只执行一项任务,则通过将maxConcurrentOperationCount 设置为1 使其成为串行队列。您也可以设置操作的优先级,qualityOfServicequeuePriority,但这些对任务优先级的影响远比依赖关系和/或队列并发程度要微妙得多。【参考方案4】:

另一个 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)有什么问题吗; 不,当然,这没有问题。事实上,你几乎不想sleep()!我只是出于教学原因添加了那些 sleep() 调用,以使块运行足够长的时间,以便您可以看到它们同时运行。在这个简单的例子中,在没有sleep() 的情况下,这两个块可能运行得非常快,以至于分派的块可能在您有机会凭经验观察并发执行之前开始和结束。但是不要在你自己的代码中sleep()【参考方案5】:

扩展 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!

【讨论】:

@ɲeuroburɳ 上面的代码在主线程上等待。我相信这会阻塞主线程并导致 UI 无响应,直到整个组完成。我建议将等待移至后台线程。例如 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) @cbartel,好收获!我已更新示例代码以反映您的评论。很多时候,您需要将回调放在主队列中——在这种情况下,尽管dispatch_queue_notify 可能更好(除非保证阻塞时间很短)。 我在哪里可以释放组(即 dispatch_release(group) )?我不确定在 dispatch_group_notify 中发布是否安全。但是由于那是组完成后运行的代码,我不确定在哪里发布。 如果你使用 ARC 那么你不需要调用 dispatch_release:***.com/questions/8618632/… 不错的帖子进一步解释了这一点:commandshift.co.uk/blog/2014/03/19/…【参考方案6】:

并不是说其他​​答案在某些情况下不是很好,但这是我始终来自 Google 的一个 sn-p:

- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel 


    if (signInDoneSel) 
        [self performSelector:signInDoneSel];
    


【讨论】:

【参考方案7】:

上面的答案都很酷,但他们都漏掉了一件事。当你使用dispatch_group_enter/dispatch_group_leave时,组在它进入的线程中执行任务(块)。

- (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 来获得您想要的。

【讨论】:

【参考方案8】:

在 Swift 5.1 中,Grand Central Dispatch 提供了多种解决问题的方法。根据您的需要,您可以选择以下 Playground sn-ps 中显示的七种模式之一。


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

Apple 开发者并发编程指南states about 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()DispatchGroupnotify(qos:flags:queue:execute:) 混合使用。


#3。使用Dispatch​Work​Item​Flags barrierDispatchQueueasync(group:qos:flags:execute:)

来自 Raywenderlich.com 的Grand Central Dispatch Tutorial for Swift 4: Part 1/2 文章给出了障碍的定义:

调度障碍是一组在处理并发队列时充当串行式瓶颈的函数。当您将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 需要同步,但不能修改时,可以使用信号量。

Apple Developer API Reference 还为 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(_:)

Apple 开发者 API 参考中提到了 Operation​Queue

操作队列使用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() 每个(并且没有信号量)?就像如果我需要等待对服务器的异步请求,然后等待第二个异步请求,依此类推。我读过这篇文章avanderlee.com/swift/asynchronous-operations,但与 BlockOperation 相比,我没有看到它的简单用法【参考方案9】:

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() 导致崩溃【参考方案10】:

迅速接受答案:

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)

【讨论】:

以上是关于等到两个异步块被执行后再开始另一个块的主要内容,如果未能解决你的问题,请参考以下文章

等待不等待前一个代码块被执行

JS问题JQUERY问题如何让一段函数执行完毕后再执行另一段函数

js执行一个方法后再执行另一个方法

NodeJS - 等到流式传输多个文件完成后再继续编写代码

如何使“主线程”等待“子线程”执行结束后再继续执行

等到一个程序完成后再在 Windows 脚本中开始下一个程序