Long-running synchronous code in Swift Concurrency

Can someone help me understand why this sample using Swift Concurrency with slow synchronous code causes the async let tasks to run serially? My understanding was that while it is not good to run slow synchronously blocking code on the concurrency thread pool, it could still run on different threads in the thread-pool at the same time. This post seems to say the same.

In this case, there are 3 tasks that could be run concurrently on different threads in the thread pool. But you can see from the logs below that the tasks run serially.

I know I could use withCheckedContinuation and explicitly dispatch the synchronous code to a GCD global queue to run concurrently, but I'm wondering why this example using only Swift Concurrency does not run the tasks concurrently. Do I need to use GCD to force long-running synchronous code to run off the concurrency thread pool? Or is there some other flaw in my code sample?

func loadAsync(_ name: String) async -> Int {
    return longRunningSync(name)
}

func longRunningSync(_ name: String) -> Int {
    print("did start \(name)")

    var count = 1
    for _ in 0...100_000 {
        count = count * count
    }

    print("did finish \(name)")
    return 1
}

// expect these to run in parallel
async let task1 = await loadAsync("1")
async let task2 = await loadAsync("2")
async let task3 = await loadAsync("3")
_ = await [task1, task2, task3]

console logs:

did start 1
did finish 1
did start 2
did finish 2
did start 3
did finish 3
2 Likes

You are awaiting before your async let. Drop those async and I think it should work as expected.

2 Likes

Besides the above correction:

If your algorithm will deadlock without parallel concurrent execution, then yes you have to use GCD. Swift concurrency supports but does not guarantee parallel execution. Some targets (notably iOS simulator, WASM, and possibly future embedded platforms) do not support parallel concurrent execution. Swift supports cooperative asynchronous execution. Any long running operation needs to periodically call Task.yield() to give up execution to let the other tasks make forward progress. If you are dealing with something like synchronous I/O, you have to use a different operating system thread (outside Swift concurrency).

2 Likes

Sorry for what is possibly an obvious question: are you saying code that uses Swift Concurrency will always execute serially on a simulator? But possibly concurrently on a physical device?

Iā€™m curious about the details of this, is this documented anywhere?

Many thanks.

:man_facepalming: Thank you for pointing that out, that fixes things when I run on an iPad. Interestingly, it still runs serially on simulator as @hooman predicted

updated:

// expect these to run in parallel
async let task1 = loadAsync("1")
async let task2 = loadAsync("2")
async let task3 = loadAsync("3")
_ = await [task1, task2, task3]

iPad Logs:

did start 2
did start 1
did start 3
did finish 1
did finish 2
did finish 3

simulator logs:

did start 1
did finish 1
did start 2
did finish 2
did start 3
did finish 3
1 Like

I updated with my sample code running on simulator and iPad, and it looks like simulator does not run in parallel. That is very interesting. Has implications for how I run unit tests designed to catch threading issues...

Simulators cooperative pool only use one thread IIUC. Run on device when you want to get real concurrency.

1 Like

Thanks for the details all! Also, yikes, this frightens me :bat::jack_o_lantern::fearful:

2 Likes