I understand that an asynchronous Swift Task
is not supposed to block (async worker threads must always make forward progress). If I have an otherwise 100% async Swift application but need to introduce some blocking tasks, what is the correct way to do this that will not block any of the swift async thread pool workers?
I'm assuming a new dedicated thread outside of the async thread pool is required, if that assumption is correct what is the thread safe way for an async function to await for that thread to complete? Can I use the body of withCheckedContinuation
to launch a thread, copy the continuation
handle into that thread and call continuation.resume
from that thread when it completes?
I understand that an asyncronous Swift Task is not supposed to block (async worker threads must always make forward progress).
This is correct. The cornerstone of the Swift concurrency system is that tasks must always be making forward progress.
Can I use the body of
withCheckedContinuation
to launch a thread, copy the continuation handle into that thread and call continuation.resume from that thread when it completes?
Yes, this is also correct, and is exactly the purpose of continuations:
CheckedContinuation
A mechanism to interface between synchronous and asynchronous code, logging correctness violations.
The purpose of a continuation is to allow you to fit blocking synchronous operations into the async
world. When you call withCheckedContinuation
, it
[s]uspends the current task, then calls the given closure with a checked continuation for the current task.
The task is suspended indefinitely until you resume it, which allows other tasks to make progress in the meantime. The continuation value you get is a thread-safe interface to indicate that your blocking operation is done, and that the original task should resume at the next available opportunity. The continuation is also Sendable, which indicates that you can safely pass it between threads. Any thread is allowed to resume the task, so you don't even necessarily need to call back to the continuation on the same thread.
An example usage from SE-0300: Continuations for interfacing async tasks with synchronous code:
func operation() async -> OperationResult {
// Suspend the current task, and pass its continuation into a closure
// that executes immediately
return await withUnsafeContinuation { continuation in
// Invoke the synchronous callback-based API...
beginOperation(completion: { result in
// ...and resume the continuation when the callback is invoked
continuation.resume(returning: result)
})
}
}
Note that this is only necessary for tasks which are truly blocking, and cannot make further progress until something they depend on is done. This is different from tasks which perform active computation which just happen to take a long time, as those are tasks which are at least making active progress. (But in chat, you clarify that your use-case is the former.)