swiftconcurrencygrand-central-dispatchconcurrent-queue

Why concurrent queue with sync act like serial queue?


Could anyone help me to understand this code I created:

let cq = DispatchQueue(label: "downloadQueue", attributes: .concurrent)
cq.sync {
for i in 0..<10 {
    sleep(2)
    print(i)
  }
}

print("all finished!")

And the output is serial order 1->10, with 2 sec waiting in between. In the end it will print out all finished

I understand that the the last part. However my question is:

Shouldn't concurrent queue start multiple tasks at the same time? So my original thought was: the 1-10 printing should be done concurrently, not necessarily in the serial order.

Could anyone explain the purpose of sync call on concurrent queue and give me an example why and when we need it?


Solution

  • If you want to demonstrate them running concurrently, you should dispatch the 10 tasks individually:

    let queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
    for i in 0..<10 {
        queue.async {
            sleep(2)
            print(i)
        }
    }
    print("all finished queuing them!")
    

    Note:


    You ask:

    So my original thought was: the 1-10 printing should be done concurrently, not necessarily in the serial order.

    Because they are inside a single dispatch, they will run as a single task, running in order. You need to put them in separate dispatches to see them run concurrently.

    You go on to ask:

    Could anyone explain the purpose of sync call on concurrent queue and give me an example why and when we need it?

    The sync has nothing to do with whether the destination queue is serial or concurrent. The sync only dictates the behavior of the calling thread, namely, should the caller wait for the dispatched task to finish or not. In this case, you really do not want to wait, so you should use async.

    As a general rule, you should avoid calling sync unless (a) you absolutely have to; and (b) you are willing to have the calling thread blocked until the sync task runs. So, with very few exceptions, one should use async. And, perhaps needless to say, we never block the main thread for more than a few milliseconds.

    While using sync on a concurrent dispatch queue is generally avoided, one example you might encounter is the “reader-writer” synchronization pattern. In this case, “reads” happen synchronously (because you need to wait the result), but “writes” happen asynchronously with a barrier (because you do not need to wait, but you do not want it to happen concurrently with respect to anything else on that queue). A detailed discussion of using GCD for synchronization (esp the reader-writer pattern), is probably beyond the scope of this question. But search the web or StackOverflow for “GCD reader-writer” and you will find discussions on the topic.)


    Let us graphically illustrate my revamped rendition of your code, using OSSignposter to create intervals in Instruments’ “Points of Interest” tool:

    import os.log
    
    private let signposter = OSSignposter(subsystem: "Demo", category: .pointsOfInterest)
    
    final class Demo: Sendable {
        func run() {
            let queue = DispatchQueue(label: "queue", attributes: .concurrent)
    
            for i in 0..<10 {
                queue.async { [self] in
                    signposter.withIntervalSignpost(#function, id: signposter.makeSignpostID(), "\(i)") {
                        spin(for: .seconds(2))
                    }
                }
            }
            signposter.emitEvent("submitted", "finished queuing them!")
        }
    
        private func spin(for duration: Duration) {
            let start = ContinuousClock.now
            while start.duration(to: .now) < duration { }
        }
    }
    

    When I profile this in Instruments (e.g. “Product” » “Profile”), choosing “Time Profiler” template (which includes “Points of Interest” tool), I see a graphical timeline of what is happening:

    time profiler…points of interest

    This illustrates, as you noted, concurrent tasks may not appear in the precise order that they were submitted. This is because (a) they all were queued so quickly in succession; and (b) they run concurrently: There is a “race” as to which concurrently running thread gets to the logging statements (or “Points of Interest” intervals) first.