When conforming to protocols or overriding superclass methods, you may not be able to change a method to be async
, but you may still want to call some async
code. For example, as I am rewriting a program to be written in terms of Swift's new structured concurrency, I would like to call some async
set-up code at the beginning of my test suite by overriding the class func setUp()
defined on XCTestCase
. I want my set-up code to complete before any of the tests run, so using Task.detached
or async { ... }
is inappropriate.
Initially, I wrote a solution like this:
final class MyTests: XCTestCase {
override class func setUp() {
super.setUp()
unsafeWaitFor {
try! await doSomeSetup()
}
}
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
This seems to work well enough. However, in Swift concurrency: Behind the scenes, runtime engineer Rokhini Prabhu states that
Primitives like semaphores and condition variables are unsafe to use with Swift concurrency. This is because they hide dependency information from the Swift runtime, but introduce a dependency in execution in your code... This violates the runtime contract of forward progress for threads.
She also includes a code snippet of such an unsafe code pattern
func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
let semaphore = DispatchSemaphore(value: 0)
async {
await asyncUpdateDatabase()
semaphore.signal()
}
semaphore.wait()
}
which is notably the exact pattern I had come up with (I find it very amusing that the code I came up with is exactly the canonical incorrect code modulo renaming).
Unfortunately, I have not been able to find any other way to wait for async code to complete from a synchronous function. Further, I have not found any way whatsoever to get the return value of an async function in a synchronous function. The only solutions I have been able to find for this on the internet seem just as incorrect as mine, for example this The Swift Dev article says that
In order to call an async method inside a sync method, you have to use the new detach function and you still have to wait for the async functions to complete using the dispatch APIs.
which I believe to be incorrect or at least unsafe.
What is a correct, safe way to wait for an async
function from a synchronous function to work with existing synchronous class or protocol requirements, unspecific to testing or XCTest? Alternatively, where can I find documentation spelling out the interactions between async
/await
in Swift and existing synchronization primitives like DispatchSemaphore
? Are they never safe, or can I use them in special circumstances?
Update:
As per @TallChuck's answer which noticed that setUp()
always runs on the main thread, I have discovered that I can intentionally deadlock my program by calling any @MainActor
function. This is excellent evidence that my workaround should be replaced ASAP.
Explicitly, here is a test which hangs.
import XCTest
@testable import Test
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
unsafeWaitFor {
try! await doSomeSetup()
}
}
}
func doSomeSetup() async throws {
print("Starting setup...")
await doSomeSubWork()
print("Finished setup!")
}
@MainActor
func doSomeSubWork() {
print("Doing work...")
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
However, it does not hang if @MainActor
is commented out. One of my fears is that if I ever call out to library code (Apple's or otherwise), there is no way to know if it will eventually call an @MainActor
function even if the function itself is not marked @MainActor
.
My second fear is that even if there is no @MainActor
, I still don't know I am guaranteed that this is safe. On my computer, this hangs.
import XCTest
@testable import Test
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
unsafeWaitFor {
print("Hello")
}
}
}
}
}
}
}
}
func unsafeWaitFor(_ f: @escaping () async -> ()) {
let sema = DispatchSemaphore(value: 0)
async {
await f()
sema.signal()
}
sema.wait()
}
If this doesn't hang for you, try adding more unsafeWaitFor
s. My development VM has 5 cores, and this is 6 unsafeWaitFor
s. 5 works fine for me. This is distinctly unlike GCD. Here is an equivalent in GCD which does not hang on my machine.
final class TestTests: XCTestCase {
func testExample() throws {}
override class func setUp() {
super.setUp()
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
safeWaitFor { callback in
print("Hello")
callback()
}
callback()
}
callback()
}
callback()
}
callback()
}
callback()
}
}
}
func safeWaitFor(_ f: @escaping (() -> ()) -> ()) {
let sema = DispatchSemaphore(value: 0)
DispatchQueue(label: UUID().uuidString).async {
f({ sema.signal() })
}
sema.wait()
}
This is fine because GCD is happy to spawn more threads than you have CPUs. So maybe the advice is "only use as many unsafeWaitFor
s as you have CPUs", but if that's the case, I would like to see somewhere that Apple has spelled this out explicitly. In a more complex program, can I actually be sure that my code has access to all the cores on the machine, or is it possible that some other part of my program is using the other cores and thus that the work requested by unsafeWaitFor
will never be scheduled?
Of course, the example in my question is about tests, and so in that case, it is easy to say "it doesn't really matter what the advice is: if it works, it works, and if it doesn't, the test fails, and you'll fix it," but my question isn't just about tests; that was just an example.
With GCD, I have felt confident in my ability to synchronize asynchronous code with semaphores (on my own DispatchQueue
s that I control, and not the main thread) without exhausting the total available threads. I would like be able to synchronize async
code from a synchronous function with async
/await
in Swift 5.5.
If something like this is not possible, I would also accept documentation from Apple spelling out in exactly what cases I can safely use unsafeWaitFor
or similar synchronization techniques.
async/await
? We couldn't have. Withoutasync/await
, we have never been able to wait, and we still can't. If we do async work duringsetUp
,setUp
will end. – NetworkDispatchSemaphore
method above, but with functions which take callbacks instead of with anasync
function. With concurrency based onDispatchQueue
, this is okay because if a queue blocks, GCD can spawn more threads to do work so that the blocked thread may be able to resume in the future. Swift’s built-in executor will not spawn new threads (at least not because of this), so it is easy for theDispatchSemaphore
method to deadlock withasync
functions, at least in theory. My set-up code is simple enough that I haven’t hit a deadlock yet. – HornesetUp()
. – Extracellularasync
for all my tests, and that's been working great. I am pretty sure switching an existing method toasync
is ABI and source-breaking, so I don't really know how Apple will go about fixingsetUp
. Hopefully there will be a safe work-around soon. – Horneasync
/await
though. – Hornejoin()
on (i.e. block waiting for) the worker threads – ExtracellularunsafeWaitFor
block argument@Sendable
fixed it. If anyone knows why please share! – Reta