You could maybe argue that asynchronous code doesn’t belong in setUp()
, but it seems to me that to do so would be to conflate synchronicity with sequential…icity? The point of setUp()
is to run before anything else begins running, but that doesn’t mean it has to be written synchronously, only that everything else needs to view it as a dependency.
Fortunately, Swift 5.5 introduces a new way of handling dependencies between blocks of code. It’s called the await
keyword (maybe you’ve heard of it). The most confusing thing about async
/await
(in my opinion) is the double-sided chicken-and-egg problem it creates, which is not really addressed very well in any materials I’ve been able to find. On the one hand, you can only run asynchronous code (i.e. use await
) from within code that is already asynchronous, and on the other hand, asynchronous code seems to be defined as anything that uses await
(i.e. runs other asynchronous code).
At the lowest level, there must eventually be an async
function that actually does something asynchronous. Conceptually, it probably looks something like this (note that, though written in the form of Swift code, this is strictly pseudocode):
func read(from socket: NonBlockingSocket) async -> Data {
while !socket.readable {
yieldToScheduler()
}
return socket.read()
}
In other words, contrary to the chicken-and-egg definition, this asynchronous function is not defined by the use of an await
statement. It will loop until data is available, but it allows itself to be preempted while it waits.
At the highest level, we need to be able to spin up asynchronous code without waiting for it to terminate. Every system begins as a single thread, and must go through some kind of bootstrapping process to spawn any necessary worker threads. In most applications, whether on a desktop, smart phone, web server, or what have you, the main thread then enters some kind of “infinite” loop where it, maybe, handles user events, or listens for incoming network connections, and then interacts with the workers in an appropriate way. In some situations, however, a program is meant to run to completion, meaning that the main thread needs to oversee the successful completion of each worker. With traditional threads, such as the POSIX pthread
library, the main thread calls pthread_join()
for a certain thread, which will not return until that thread terminates. With Swift concurrency you….. can’t do anything like that (as far as I know).
The structured concurrency proposal allows top-level code to call async
functions, either by direct use of the await
keyword, or by marking a class with @main
, and defining a static func main() async
member function. In both cases, this seems to imply that the runtime creates a “main” thread, spins up your top-level code as a worker, and then calls some sort of join()
function to wait for it to finish.
As demonstrated in your code snippet, Swift does provide some standard library functions that allow synchronous code to create Task
s. Tasks are the building block of the Swift concurrency model. The WWDC presentation you cited explains that the runtime is intended to create exactly as many worker threads as there are CPU cores. Later, however, they show the below image, and explain that a context switch is required any time the main thread needs to run.
As I understand it, the mapping of threads to CPU cores only applies to the “Cooperative thread pool”, meaning that if your CPU has 4 cores, there will actually be 5 threads total. The main thread is meant to remain mostly blocked, so the only context switches will be the rare occasions when the main thread wakes up.
It is important to understand that under this task-based model, it is the runtime, not the operating system, that controls “continuation” switches (not the same as context switches). Semaphores, on the other hand, operate at the operating system level, and are not visible to the runtime. If you try to use semaphores to communicate between two tasks, it can cause the operating system to block one of your threads. Since the runtime cannot track this, it will not spin up a new thread to take its place, so you will end up under-utilized at best, and deadlocked at worst.
Okay, finally, in Meet async/await in Swift, it is explained that the XCTest
library can run asynchronous code “out of the box”. However, it is not clear whether this applies to setUp()
, or only to the individual test case functions. If it turns out that it does support an asynchronous setUp()
function, then your question is suddenly entirely uninteresting. On the other hand, if it does not support it, then you are stuck in the position that you cannot directly wait on your async
function, but that it’s also not good enough just to spin up an unstructured Task
(i.e. a task that you fire and forget).
Your solution (which I see as a workaround — the proper solution would be for XCTest
to support an async
setUp()
), blocks only the main thread, and should therefore be safe to use.