Search code examples
swiftmultithreadinggrand-central-dispatch

In GCD are serial queue synchronous by asynchronous operation Swift


I am using a serial queue with QoS background

let serialQueue = DispatchQueue(label: "queue1", qos: DispatchQoS.background)

Assigning one jobs synchronous, and two jobs asynchronous:

func serialTask() {
    serialQueue.sync {
        for i in 0..<10 {
            print("🔷", i)
        }
    }
    serialQueue.async {
        for i in 20..<30 {
            print("⚪️", i)
        }
    }

    serialQueue.async {
        for i in 101..<120 {
            print("🔷", i)
        }
    }
}

All 3 jobs are executing sync one after another, but last two jobs are async. Are async jobs sync in serial queue's.


Solution

  • 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.