swiftconcurrencydispatch-asyncdispatch-queue

Why the second doesn't start before the first task is finished? (Concurrent Queue)


Concurrent dispatch queue:

func myConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
    concurrentQueue.async {
        print("Task 1 Con Start")
        print("Task 1 Con Finished")
    }
    concurrentQueue.async {
        print("Task 2 Con Start")
        print("Task 2 Con Finished")
    }
}

myConcurrentQueue()

I guess the result should be:

Task 1 Con Start
Task 2 Con Start
Task 1 Con Finished
Task 2 Con Finished

But the result is shown as picture below

enter image description here

As a result, I have 2 problems from the issue above.

  1. Why does the concurrent queue return a result like this?
  2. Does label in DispatchQueue affect the result?

Solution

  • The dispatched work does not take long enough to experience parallel execution. The first one may run so quickly that it finishes before the second has a chance to start. (And the fact that print output is synchronized, exacerbates this.) Try inserting a Thread.sleep(…) (which you’d never do in production code, but is useful for diagnostic purposes) or something time-consuming.

    For example:

    func myConcurrentQueue() {
        let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
        let start = ContinuousClock().now
        concurrentQueue.async {
            print("Task 1 Con Start at", start.duration(to: .now))
            Thread.sleep(forTimeInterval: 1)                           // never do this in production app; just for diagnostic purposes
            print("Task 1 Con Finished at", start.duration(to: .now))
        }
        concurrentQueue.async {
            print("Task 2 Con Start at", start.duration(to: .now))
            Thread.sleep(forTimeInterval: 1)                           // never do this in production app; just for diagnostic purposes
            print("Task 2 Con Finished at", start.duration(to: .now))
        }
    }
    

    I might suggest including some information to show information about when the output was generated (hence my use of the ContinuousClock in the above).

    Anyway, that produces something like:

    Task 1 Con Start at 0.001328375 seconds
    Task 2 Con Start at 0.001392083 seconds
    Task 2 Con Finished at 1.006529791 seconds
    Task 1 Con Finished at 1.006475916 seconds
    

    Note, because these run in parallel, you technically have no assurances regarding which item reaches its respective print statements first (thus in this example, task 2 happened to finish a fraction of a millisecond before task 1), but you can see them run in parallel.

    And, in answer to your question, no, the choice of label does not matter. That is used for diagnostic purposes only.


    Also, note that just because you use a concurrent queue does not guarantee that you will necessarily experience parallel execution. It depends upon whether the other cores are busy doing something else, or not. Also, I’d be wary of testing this sort of stuff in playgrounds (if you are) as that can sometimes exhibit execution characteristics that are quite atypical of a real app.


    Another way to visualize the parallel execution is to use the “Points of Interest” tool in Instruments as outlined in How to identify key events in Xcode Instruments?

    import os.signpost
    
    let poi = OSSignposter(subsystem: "Test", category: .pointsOfInterest)
    
    func myConcurrentQueue() {
        let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
        concurrentQueue.async {
            poi.withIntervalSignpost("1") {
                Thread.sleep(forTimeInterval: 1)                       // never do this in production app; just for diagnostic purposes
            }
        }
        concurrentQueue.async {
            poi.withIntervalSignpost("2") {
                Thread.sleep(forTimeInterval: 1)
            }
        }
    }
    

    That lets us visualize the concurrent queue’s behavior on a timeline from within Instruments, and you can see they run in parallel. But again, we only see that because I had it do something slow enough such that the first work item likely will not finish before the second work item has a chance to start:

    enter image description here


    Note, nowadays we would not generally reach for GCD concurrent queues. We would instead use Swift concurrency, and in this case, a “task group”:

    func myTaskGroup() async {
        await withTaskGroup(of: Void.self) { group in
            group.addTask {   // first task
                …
            }
            group.addTask {   // second task
                …
            }
        }
    }
    

    For more information, see WWDC 2021 video Meet async/await in Swift. And task groups are introduced at 12:52 in Explore structured concurrency in Swift.

    Bottom line, Swift concurrency is beyond the scope of the question, but I wanted to share the modern alternative to concurrent dispatch queues.