In GCD are serial queue synchronous by asynchronous operation Swift

Synchronous and asynchronous execution has nothing to do with the underlying queue. Synchronous execution means, that the calling thread must wait until the block is finished. Thus, the second block is enqueued after the first block has finished. Asynchrounous means, that the caller must not wait for the completion of the block. Thus, the third block is enqueued directly after the preceeding serialQueue.async statement while the second block is still running or even waiting for execution.

At the end of your function serialTask() it is guaranteed that the first block is executed. The second and the third blocks are enqueued, but it is not sure if they are executed, running or even waiting for execution. Since you're using a serial queue, it is sure that the second block is executed before the third block.

You may check the asynchronous execution of the two last blocks by adding

serialQueue.async {
    print "sleep"
    sleep(10);
    print "awake"
}

before the two calls of async, and you will observe the following:

  1. The first block will be executed immediately, and sleep will be printed immediately.
  2. The execution of serialTask() takes considerably less than 10 seconds.
  3. awake is only output after 10 seconds (surprise, surprise).
  4. The second and the third block are also executed after 10 seconds, so long after serialTask() ended.

This late execution of the two blocks means asynchronous. If you're moving your first (synchronous) block to the end of serialTask(), the following will happen:

  1. sleep will be printed immediately.
  2. The execution of serialTask() takes approximately 10 seconds. It is finished after the synchronous block at the end is executed.
  3. awake is only output after 10 seconds (surprise, surprise).
  4. The second and the third block are also executed after awake is printed out.

Let me see if I can clarify the difference between async vs. sync.

A couple of changes that I will employ in my example:

  1. I will use Instruments’ “Points of Interest” to show when tasks are running rather than print statements. (See WWDC 2019 Getting Started With Instruments.) This way we can see the behavior graphically.

    I will post a simple “Point of Interest” event signpost (Ⓢ) when dispatching something and I will wrap the dispatched task in a “Region of Interest” (a horizontal bar) to graphically illustrate the duration of some process.

  2. I'll change your for loops to be a Thread.sleep(forTimeInterval: 1), simulating some time consuming process. If you just have a quick for loop, things will happen so quickly that it will be impossible to discern what's really happening with the threads.

So, consider:

import os.signpost

private let pointsOfInterest = OSLog(subsystem: "GCD Demo", category: .pointsOfInterest)

func tasks(on queue: DispatchQueue) {
    pointsOfInterestRange(with: "tasks(on:)") {
        os_signpost(.event, log: pointsOfInterest, name: "1") // first Ⓢ
        queue.sync { self.oneSecondProcess(with: "1") }

        os_signpost(.event, log: pointsOfInterest, name: "2") // second Ⓢ
        queue.async { self.oneSecondProcess(with: "2") }

        os_signpost(.event, log: pointsOfInterest, name: "3") // third Ⓢ
        queue.async { self.oneSecondProcess(with: "3") }
    }
}

func oneSecondProcess(with staticString: StaticString) {
    pointsOfInterestRange(with: staticString) {
        Thread.sleep(forTimeInterval: 1)
    }
}

func pointsOfInterestRange(with staticString: StaticString, block: () -> Void) {
    let identifier = OSSignpostID(log: pointsOfInterest)
    os_signpost(.begin, log: pointsOfInterest, name: staticString, signpostID: identifier)
    block()
    os_signpost(.end, log: pointsOfInterest, name: staticString, signpostID: identifier)
}

That is just like your example, but rather than print statement, we have signposts statements, yielding the following graphical timeline in Instruments’ “Points of Interest” tool:

enter image description here

So, you can see that:

  1. The tasks(on:) function, on the bottom, issued the sync dispatch, the first Ⓢ signpost.

  2. It waits for the sync task, “1”, to finish before continuing, at which point it issues the two subsequent dispatches, the second and third Ⓢ signposts (which happen so quickly in succession that they overlap in the graph).

  3. But tasks(on:) doesn't wait for the two async tasks, “2” and “3”, to finish. As soon as it finished dispatching those async tasks, it immediately returns (hence the tasks(on:) range stops immediately at that point).

  4. Because the background queue was serial, the three dispatched tasks (“1”, “2”, and “3”) run sequentially, one after the other.

If you change this to use a concurrent queue, though:

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

Then you can see that the two async tasks now run concurrently with respect to each other:

enter image description here

This time, task(on:) dispatches the sync call, waits for it to finish, and then, only when that sync call is done can seriesOfTasks proceed to dispatch the two async calls (in this case, not waiting for those to dispatched tasks to finish).

As you can see, the async and sync behavior is different. With sync the calling thread will wait for the dispatched task to finish, but with async, it won't.


There are two main conclusions that one can draw from the above:

  1. The choice of sync vs async dictates the behavior of the current thread (i.e. should it wait for the dispatched task or not).

    And, as a general rule, we would generally avoid calling sync from the main thread when doing anything time consuming (because that would end up blocking the main thread).

  2. The choice of a serial queue vs a concurrent queue dictates the behavior of the work you dispatched, namely can it run concurrently with respect to other tasks on that queue, or will they run consecutively, one after the other.