How does a serial queue/private dispatch queue know when a task is complete?

故事扮演 提交于 2019-12-21 04:31:45

问题


(Perhaps answered by How does a serial dispatch queue guarantee resource protection? but I don't understand how)

Question

How does gcd know when an asynchronous task (e.g. network task) is finished? Should I be using dispatch_retain and dispatch_release for this purpose? Update: I cannot call either of these methods with ARC... What do?

Details

I am interacting with a 3rd party library that does a lot of network access. I have created a wrapper via a small class that basically offers all the methods i need from the 3rd party class, but wraps the calls in dispatch_async(serialQueue) { () -> Void in (where serialQueue is a member of my wrapper class).

I am trying to ensure that each call to the underlying library finishes before the next begins (somehow that's not already implemented in the library).


回答1:


The serialisation of work on a serial dispatch queue is at the unit of work that is directly submitted to the queue. Once execution reaches the end of the submitted closure (or it returns) then the next unit of work on the queue can be executed.

Importantly, any other asynchronous tasks that may have been started by the closure may still be running (or may not have even started running yet), but they are not considered.

For example, for the following code:

dispatch_async(serialQueue) {
    print("Start")
    dispatch_async(backgroundQueue) {
       functionThatTakes10Seconds()
       print("10 seconds later")
    }
    print("Done 1st")
}

dispatch_async(serialQueue) {
    print("Start")
    dispatch_async(backgroundQueue) {
       functionThatTakes10Seconds()
       print("10 seconds later")
    }
    print("Done 2nd")
}

The output would be something like:

Start

Done 1st

Start

Done 2nd

10 seconds later

10 seconds later

Note that the first 10 second task hasn't completed before the second serial task is dispatched. Now, compare:

dispatch_async(serialQueue) {
    print("Start")
    dispatch_sync(backgroundQueue) {
       functionThatTakes10Seconds()
       print("10 seconds later")
    }
    print("Done 1st")
}

dispatch_async(serialQueue) {
    print("Start")
    dispatch_sync(backgroundQueue) {
       functionThatTakes10Seconds()
       print("10 seconds later")
    }
    print("Done 2nd")
}

The output would be something like:

Start

10 seconds later

Done 1st

Start

10 seconds later

Done 2nd

Note that this time because the 10 second task was dispatched synchronously the serial queue was blocked and the second task didn't start until the first had completed.

In your case, there is a very good chance that the operations you are wrapping are going to dispatch asynchronous tasks themselves (since that is the nature of network operations), so a serial dispatch queue on its own is not enough.

You can use a DispatchGroup to block your serial dispatch queue.

dispatch_async(serialQueue) {
    let dg = dispatch_group_create()
    dispatch_group_enter(dg)
    print("Start")
    dispatch_async(backgroundQueue) {
       functionThatTakes10Seconds()
       print("10 seconds later")
       dispatch_group_leave(dg)
    }
    dispatch_group_wait(dg)
    print("Done")
}

This will output

Start

10 seconds later

Done

The dg.wait() blocks the serial queue until the number of dg.leave calls matches the number of dg.enter calls. If you use this technique then you need to be careful to ensure that all possible completion paths for your wrapped operation call dg.leave. There are also variations on dg.wait() that take a timeout parameter.




回答2:


As mentioned before, DispatchGroup is a very good mechanism for that.

You can use it for synchronous tasks:

let group = DispatchGroup()
DispatchQueue.global().async(group: group) {
   syncTask()
}

group.notify(queue: .main) {
    // done
}

It is better to use notify than wait, as wait does block the current thread, so it is safe on non-main threads.

You can also use it to perform async tasks:

let group = DispatchGroup()
group.enter()
asyncTask {
   group.leave()
}

group.notify(queue: .main) {
    // done
}

Or you can even perform any number of parallel tasks of any synchronicity:

let group = DispatchGroup()

group.enter()
asyncTask1 {
   group.leave()
}

group.enter() //other way of doing a task with synchronous API
DispatchQueue.global().async {
   syncTask1()
   group.leave()
}

group.enter()
asyncTask2 {
   group.leave()
}

DispatchQueue.global().async(group: group) {
   syncTask2()
}

group.notify(queue: .main) {
    // runs when all tasks are done
}

It is important to note a few things.

  1. Always check if your asynchronous functions call the completion callback, sometimes third party libraries forget about that, or cases when your self is weak and nobody bothered to check if the body got evaluated when self is nil. If you don't check it then you can potentially hang and never get the notify callback.
  2. Remember to perform all the needed group.enter() and group.async(group: group) calls before you call the group.notify. Otherwise you can get a race condition, and the group.notify block can fire, before you actually finish your tasks.

BAD EXAMPLE

let group = DispatchGroup()

DispatchQueue.global().async {
   group.enter()
   syncTask1()
   group.leave()
}

group.notify(queue: .main) { 
    // Can run before syncTask1 completes - DON'T DO THIS
}



回答3:


The answer to the question in your questions body:

I am trying to ensure that each call to the underlying library finishes before the next begins

A serial queue does guarantee that the tasks are progressed in the order you add them to the queue.


I do not really understand the question in the title though:

How does a serial queue ... know when a task is complete?



来源:https://stackoverflow.com/questions/42145427/how-does-a-serial-queue-private-dispatch-queue-know-when-a-task-is-complete

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!