1

I’m trying to understand swift sync/async with the below snippet, but the variables did not add, and the iteration times of the loop varied every time. The outputs were different too.

var mainAsync: Int = 1
var queueAsync: Int = 10
var queueAsyncTwo: Int = 20
var mainAsyncTwo: Int  = 30
let queue = DispatchQueue(label:"com")

DispatchQueue.main.async {
    for i in 0..<10 { mainAsync += 1 }
}

queue.async {
    for i in 0..<10 {
        queueAsync += 1
    }
}

queue.async {
    for i in 0..<10 {
        queueAsyncTwo += 1
    }
}

for i in 0..<10 {
    mainAsyncTwo += 1
}

print("\(mainAsync),-- \(queueAsync),-- \(queueAsyncTwo),--\(mainAsyncTwo)")

The expected outputs:

10,-- 20,-- 30,-- 40

The real outputs varied from time to time like:

1,-- 19,-- 20,--40
1,-- 20,-- 22,--40
1,-- 20,-- 25,--40

I removed the print(thread.current). It wasn’t working to represent the loop times.

3
  • 3
    DispatchQueue is being replaced by the new concurrency. I suggest looking at "Meet async/await" and all the newer videos Commented Jun 24 at 17:06
  • "I’m trying to understand swift sync/async" These sync and async methods on DispatchQueue are just that... methods from the Dispatch library. They don't really have anything to do with Swift specifically (the same methods are callable from C and Objective C, under similar names). I think you're confusing this with Swift's async/await feature, like @loremipsum mentioned. Commented Jun 24 at 21:06
  • Adding to what Alexander says, Swift's async/await language features are meant to make asynchronous code behavior more intuitive and resolve exactly the type of confusion you are experiencing. I strongly suggest you switch gears and work with async/await instead. Commented Jun 25 at 13:44

2 Answers 2

1

These are asynchronous operations - they might not have been completed by the time print is called at the final line. This is a common mistake. For example, the code in this question sends a request to some URL, which is an asynchronous operation. Similar to your code, the code in that question does not wait for the response to be received before printing the contents of the response, and so nothing gets printed.

In particular, the next line after the queue.async { ... } call is executed immediately after the job is submitted to the queue, not after the job has been completed. Compare this to queue.sync { ... } which will only return after the job has been completed.

As for the job you scheduled on DispatchQueue.main, it is never executed. This is because the main queue is special - it will not execute anything you submit to it unless you do one of the following (from the documentation):

  • Calling dispatchMain()
  • Starting your app with a call to UIApplicationMain(_:_:_:_:) (iOS) or NSApplicationMain(_:_:) (macOS)
  • Using a CFRunLoop on the main thread

You did not do any of these things.

Note that the loop that increments mainAsyncTwo is not a job submitted to the main queue. It is just run in the main thread.

You can do the third bullet point above - run the main RunLoop for some time before you print:

...

RunLoop.main.run(until: .now.addingTimeInterval(1))

print("\(mainAsync),-- \(queueAsync),-- \(queueAsyncTwo),--\(mainAsyncTwo)")

This not only causes the main queue to execute its jobs, and also gives enough time for all the queues to finish everything you submitted to them.

Now this prints 11,-- 20,-- 30,--40.

Sign up to request clarification or add additional context in comments.

Comments

1

As Sweeper said, the first three are working asynchronously (are called later). You want to check the results only after those finished. The typical GCD pattern to have the asynchronous work use a DispatchGroup, which can notify you went it’s done:

var valueOne = 1
var valueTwo = 10
var valueThree = 20
var valueFour = 30

let queue = DispatchQueue(label: "serialQueue")
let group = DispatchGroup()

DispatchQueue.main.async(group: group) {          // loop 1
    for _ in 0..<10 {
        valueOne += 1
    }
}

queue.async(group: group) {                       // loop 2
    for _ in 0..<10 {
        valueTwo += 1
    }
}

queue.async(group: group) {                       // loop 3
    for _ in 0..<10 {
        valueThree += 1
    }
}

for _ in 0..<10 {                                 // loop 4
    valueFour += 1
}

group.notify(queue: .main) {
    print("\(valueOne),-- \(valueTwo),-- \(valueThree),--\(valueFour)")
}

That runloop trick that Sweeper shared with you is necessary if you are doing this GCD work in a command line app, but otherwise, DispatchGroup is the idiomatic pattern.

Please forgive the unrelated edits, but focus on the DispatchGroup and using notify to let you know when it is done.


This may be too much to take in at this point (and if so, set the following aside and come back to it later), but we should note a few problems in the above:

  1. We are declaring mutable variables on one thread and updating them on another; this is a “data race”. We would make sure to manually synchronize our access to this shared mutable state. When you start to use Swift 6, the compiler will prevent you from using this brittle pattern (a shared mutable state without synchronization).

  2. Nowadays, folks would use Swift concurrency (async-await), not GCD. The use of await eliminates the “how do I know when the asynchronous work is done” problem of your original question. No spinning on a run loop or dispatch group is required. And actors can be used to address the data race, too.

So, in Swift concurrency, to perform multiple tasks in parallel, we would use async let. And to know when the work was done, we would await the result:

@MainActor
func experiment() async {
    let experimentActor = ExperimentActor()

    async let valueOne = one()                     // loop 1
    async let valueTwo = experimentActor.two()     // loop 2
    async let valueThree = experimentActor.three() // loop 3

    var valueFour = 30                             // loop 4
    for _ in 0..<10 {
        valueFour += 1
    }

    await print("\(valueOne),-- \(valueTwo),-- \(valueThree),--\(valueFour)")
}

For clarity’s sake, I’ve moved these first three loops into their own functions, the first isolated to the main actor, the latter two isolated to their own actor:

@MainActor
func one() async -> Int {
    var value = 1

    for _ in 0..<10 {
        value += 1
    }

    return value
}

// The `ExperimentActor` is an `actor` that will let tasks 2 and 3 run concurrently
// with work on the main actor, but will make sure that 2 and 3 do not run in parallel
// with each other:

actor ExperimentActor {
    func two() async -> Int {
        var value = 10

        for _ in 0..<10 {
            value += 1
        }

        return value
    }

    func three() async -> Int {
        var value = 20

        for _ in 0..<10 {
            value += 1
        }

        return value
    }
}

Anyway, I added a little instrumentation to watch this in Instruments’ “Points of Interest” tool:

enter image description here

We can see that:

  1. The first task doesn’t run until the fourth loop finishes (because we are on the main actor, and the first task, also isolated to the main actor, doesn't have a chance to run until experiment reaches its first suspension point, the await).
  2. The second task starts immediately.
  3. The third task starts after the second one finishes (because it was on the same actor as the second task).
  4. The work we ran synchronously (the fourth loop) starts immediately.
  5. That ⓢ is where we finished, after we awaited tasks 1-3.

This is thread-safe and uses await to know when all of our asynchronous work is done.

Now, I recognize that I have eliminated one aspect of your original question, namely how to have multiple threads mutating some shared state (your four variables). But thread-safe programming dictates that we never have different threads mutating some unsynchronized shared state (i.e., we avoid this “data race”). If that really was an important part of your question, I might suggest watching Protect mutable state with Swift actors and Eliminate data races using Swift Concurrency if you want to get your arms around this aspect of the language.

1 Comment

Once again, great answer, Rob! I always appreciate the lengths you go to, especially profiling and attaching screenshots of the tracks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.