Can someone recommend a good, modern tutorial on concurrency?

It "blocks" only because your code here is sequential (precisely what structured concurrency is about), and no other work is scheduled.

BTW, that's not allowed in Swift 6 – because in this way it actually blocks a thread. You have to use Task.sleep instead, which won't block:

And then, if you'll update it properly – so that there is other work to run, it will run it:

import Foundation

func asyncFunc () async {
    try? await Task.sleep(for: .seconds(5))
    print("async func output")
}

print("will call async func")
await withDiscardingTaskGroup { group in
    group.addTask {
        await asyncFunc()
    }
    group.addTask {
        print("some other work here")
    }
}
print("did call async func")

Your main code at the top-level is still executes sequentially, since that's the goal after all. But sequential execution is not the same as blocking.

Swift saves call stack on suspension point, freeing thread for execution, and then restores when task is ready to be resumed. If Swift would block threads on each suspension point, paired with limited pool of threads it operates on, Swift Concurrency would run out of threads to execute tasks pretty easily. That's why blocking threads (and relative APIs) aren't suitable for use.

async functions are compiled into coroutines, which are basically state machines, where every synchronous part ("the code between await statements") is a separate partial synchronous function. When an async function suspends, one of those partial functions has finished, and the caller thread is running the runtime code at this point — the thread never pauses "inside" of the body of an async function.

Related: https://youtu.be/wyAbV8AM9PM and https://youtu.be/H_K-us4-K7s

5 Likes

Any single thread is sequential. And await suspends that thread IMHO. Maybe there are cooperative tasks in Swift that run on main thread (and do not create worker threads), but that is another story.

Exactly what I said.

What is "the thread"? Is it a worker thread created specifically for the async function? Or is it a caller thread using cooperative multitasking? Either way, the await in the caller thread pauses its execution.

import Foundation

func asyncFunc () async -> Int  {
    try! await Task.sleep(for: .seconds(5))
    print("async func output")
    return 100
}

print("will call async func")
async let result = asyncFunc()
print("did call async func")
print("result \(await result)")

will call async func
did call async func
async func output
result 100
Program ended with exit code: 0

2 Likes

"The thread" is either the main thread or one of the threads in the cooperative thread pool created by the concurrency runtime. The total number of these is typically equal to the number of CPU cores.

FWIW there is a certain "task-to-thread model" that can be encountered in the runtime source, but I'm not sure where it's used (embedded perhaps?). Anyway, this is not the default behaviour.

1 Like

This essentially creates a worker thread and continues caller thread execution. We here talking about the await keyword which IMHO suspends current thread, and everyone tells me otherwise.

This is not a question of opinion :) You could either look into the source that deals with suspension or watch the talks that specifically discuss how Swift's concurrency runtime is implemented.

1 Like

You say that await blocks, I say it does not and prohibits blocking APIs to be called as well to prevent this. Two opposite things. Blocking inside async function considered to be a programming error.

Pauses function execution. Thread itself is free to schedule any other work after suspension.

This is not the case. If we narrowly look at the main thread/queue/actor case then the following code:

func getX() async { ... }

@MainActor
func f() async -> Something {
  let x = await getX()
  return doSomething(x)
}

can be understood as being roughly analogous so something like:

func getX(completion: (X) -> Void)) {
  DispatchQueue.global().async {
    ...
  }
}

func g(completion: (Something) -> Void) {
  getX(completion: { x in
    DispatchQueue.main.async {
      completion(doSomething(x, y)
    }
  })
}

(Edit: updated to relocate the move off the main thread into getX)

The main thread is not blocked during an await in f any more than it is blocked after the getX call before the completion parameter is called. And moreover, all of the wrangling to move work on/off the main thread is handled 'automatically' by virtue of the functions declaring where they should be run.

2 Likes

I say that await blocks the caller thread.

This sounds like complete gibberish to me.

First, I said that await blocks caller thread, and not a called async function, which runs in a separate worker thread.

Second, blocking inside async function cannot be a programming error. You create an async function to do a lengthy processing, e.g. fetching some resource from the Web. The fetch call inside the async function is definitely an await call which blocks the async function (in other words blocks the worker thread created specifically for the async function). The whole point of creating async functions (read: worker threads) is to not block the caller thread (possibly main thread), and move blocking operations into parallel threads.

A nonisolated async function doesn't have its own dedicated thread. Nonisolated async functions are serviced by the Swift Concurrency cooperative thread pool which has a limited number of threads to service jobs submitted to the pool. When an async function hits a suspension point (i.e., an await) it is removed from scheduling contention and the thread is freed up to run other jobs submitted to the pool until the original function is unsuspended and eligible for scheduling again.

4 Likes

So Thread.sleep being marked as unavailable from async is gibberish?

I wouldn't be so sure:
https://forums.swift.org/t/cooperative-pool-deadlock-when-calling-into-an-opaque-subsystem/

await does not block any thread at all. It suspends function[1]. Then work is scheduled to be run on a thread.


  1. to be completely correct, it marks a potential suspension point, but does not determines if it will actually suspend. ↩ī¸Ž

2 Likes

Concurrency != multi-threading. You can easily get concurrency and async/await working in a single-threaded environment. In fact Node.js and browser environment without web workers is purely single-threaded, with async/await and coroutine transformation working similarly to what the Swift compiler does under the hood. Similarly, async/await in Python can be backed with a single-threaded event-loop, no threads are blocked by await.

As for Swift itself, certain platforms it targets like WASI can be single-threaded (see SWIFT_CONCURRENCY_GLOBAL_EXECUTOR=singlethreaded setting), and async/await works perfectly fine there, no threads are blocked, since blocking the only thread would just hang the whole program.

6 Likes

At some point (still?) IIRC that setting was also enabled when running apps in the iOS simulator to make it very obvious when you're introducing a thread-blocking dependency between tasks.

That is just implementation details. Dedicated thread or reused from a pool - either way it's a different thread.

As I already said, any single thread is sequential. Suspending a function in a single thread means suspending the thread.

Yes. On a different thread, created or reused from a pool specifically for the called async function. A caller thread is awaiting, that is blocked or suspended or whatever - it does not execute until the async function is finished.

There's a simple way to demonstrate that this is not what's happening:

import Foundation

@MainActor
func test() async {
    assert(Thread.isMainThread)
    await Task.yield()
    assert(Thread.isMainThread)
    try? await Task.sleep(for: .seconds(1))
    assert(Thread.isMainThread)
}

assert(Thread.isMainThread)
await test()
assert(Thread.isMainThread)
2 Likes

Means suspending the function, not thread. Thread can run other work (as @Max_Desiatov made an example of single-threaded environment). If you suspend the thread, then this thread cannot perform other tasks, which is not the case.

Exactly – reused from the pool. Caller thread can be reused from the pool after function execution has been suspended.

When you call DispatchQueue.global().async from main thread, you do not block main thread unless work is done, main thread becomes available almost immediately. await work in exactly same way as @Jumhyn has illustrated few posts ago.