Can someone recommend a good, modern tutorial on concurrency?

There is a limited number of threads in the cooperative pool, but you may have an unbounded number of tasks scheduling jobs on those threads without any resource exhaustion. Even if N+1 tasks are suspended for a pool of N threads, additional jobs will be able to run.

Additionally, the thread which picks up a continued async execution after an await may not be the same as the thread which was executing the function prior to the await! This is only guaranteed for some specific executors (e.g., MainActor guarantees its jobs are always run on the main thread).

2 Likes

This puzzles me to be honest.

Which function?

import Foundation

func doSomething() async {
    print("I'm the only function here, and I'm not suspended!")
}

await doSomething();

If the @MainActor is removed, then the async function is executed in a thread other than main. I don't quite understand how the @MainActor works.

Yes, because in this case it becomes non-isolated, i.e. does not have a specific actor bound (and the main actor is always scheduled on the main thread).

But preserving threads has nothing to do with the main actor specifically. Here's another simple test:

import Foundation

func bar() async {
    print(pthread_self())
}

func test() async {
    print(pthread_self())
    await bar()
    print(pthread_self())
}

await test()

While generally async functions can start and resume on any thread, when there's no reason to perform a thread switch, the runtime won't do so.

2 Likes

My understanding now that the await does not suspend the thread per se, but blocks it by running some kind of a run loop (or event loop). It this the case?

And where is that exactly running? suppose inside some sort of main function generated by the compiler?

It is running in a main thread, which is suspended blocked by the await.

I think a good mental model of what's going on under the hood is "everything is a queue". Nothing in Swift is ever blocked except for very short periods of time when the concurrency engine accesses one of the queues (and unless you use "old school" synchronization in your code explicitly of course).

So await f() just means the rest of the function is a callback/closure that will be called when f() posts a result in the queue. The beauty of structured concurrency is that it provides a really nice high-level syntactic model for a queue-based multithreaded architecture, and makes it much safer to use.

1 Like

And what exactly main thread is running? Suppose you have to manually write something like

Thread.mainThread.run(???)

What it should run?

Not only that. It also suspends or blocks the caller thread.

It's not a thread, and I would be careful with the words "suspend" and "block". Again, nothing is suspended or blocked in your code in its lower-level multithreading sense of the word. Your code is executed in chunks, however how exactly, or how many threads are used for executing it shouldn't concern you. Normally the concurrency engine starts a a thread pool with as many threads as there are CPU cores, then tries to use the entire pool as efficiently as possible.

2 Likes

OK. If you say that await does not block caller thread, please provide a simple code example which can demonstrate that. Please call an async function from the main thread using await. Let the async function do something time-consuming, taking e.g. 5 seconds. And make the main thread do something in the same 5 seconds, not later. E.g. just print("I am not blocked").

At this point I'm personally quite curious why you would think that blocking or suspending a whole thread is necessary for a concurrency runtime to work — besides the fact that we've already demonstrated that it doesn't happen in practice and that it would be impossible to run more than 10-ish concurrent tasks, given that the thread pool is constrained by the number of cores — this would simply be a tremendously inefficient implementation.

I'd personally have a plausible suspicion that perhaps the language developers know their stuff and have implemented the feature in a way that doesn't require requesting/switching to a new thread every ten microseconds or so.

1 Like

I do not think that it is necessary. I thought that this is how it works. OK, it works differently, and I would like to know how, at least approximately. The fact is, the await keyword allows sequential execution where the caller thread seemingly is, well, awaitng and cannot continue.

I will then remind you of the two talks by the developers of Swift I've linked above, where the runtime and compilation behaviour are discussed very extensively:

1 Like

You haven't answered my question: if you say there is no function to suspend in this code:

import Foundation

func doSomething() async {
    print("I'm the only function here, and I'm not suspended!")
}

await doSomething();

How'd you run this on main thread if that requires something like this to call?

Thread.mainThread.run(???) // what's instead of question-marks?

:man_facepalming: Elvis Has Left the Building

Update: I learned from this discussion that "flow of execution" != "thread", and that cooperative multitasking is not a thing of the past. Sorry for confusion and thanks to everyone who helped enlighten me.
:wave:

2 Likes
import Foundation

@MainActor
func f() async {
    print("f before: \(Thread.isMainThread)")

    Task { @MainActor in
        try await Task.sleep(for: .seconds(1))
        print("not blocked, is main: \(Thread.isMainThread)")
        try await Task.sleep(for: .seconds(1))
        print("not blocked, is main: \(Thread.isMainThread)")
        try await Task.sleep(for: .seconds(1))
        print("not blocked, is main: \(Thread.isMainThread)")
        try await Task.sleep(for: .seconds(1))
        print("not blocked, is main: \(Thread.isMainThread)")
    }

    try? await Task.sleep(for: .seconds(5))
    print("f after: \(Thread.isMainThread)")
}

await f()

This prints:

f before: true
not blocked, is main: true
not blocked, is main: true
not blocked, is main: true
not blocked, is main: true
f after: true
6 Likes

For the future references: that's kinda rude not to answer question in the conversation, while you expect your questions to be answered regardless

Just for the sake of example:

Worker-waiter pair
@MainActor
func waiter() async {
    try? await Task.sleep(for: .seconds(5))
    print("I'm too old for this sh*t")
}

@MainActor
func worker() async {
    for i in 0..<10 {
        print(i)
        try? await Task.sleep(for: .milliseconds(100))
    }
}

@MainActor
func main() async {
    await withDiscardingTaskGroup { group in
        group.addTask { @MainActor in
            await waiter()
        }
        group.addTask { @MainActor in
            await worker()
        }
    }
}

await main()

Printing:

0
1
2
3
4
5
6
7
8
9
I'm too old for this sh*t

@ibex10 proposed even better illustration (and there is no limit!):

Improved version of worker-waiter
@MainActor
func waiter() async {
    try? await Task.sleep(for: .seconds(3))
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
    print("I'm too old for this sh*t")
}

@MainActor
func worker() async {
    for i in 0..<10 {
        print(i)
        try? await Task.sleep(for: .seconds(1))
    }
}

@MainActor
func other_main() async {
    await withDiscardingTaskGroup { group in
        group.addTask { @MainActor in
            await waiter()
        }
        group.addTask { @MainActor in
            await worker()
        }
    }
}
1 Like