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).
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:
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?
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.
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.
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.
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:
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.
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()
}
}
}