My overal goal is to execute structured concurrency code from a synchronous context. This goal is typically the result of interop with contexts that do not support structured concurrency, such as C APIs.
There’s been discussion related to this topic on frequent occasions (example), but typically these discussions focus on using locks as a mechanism to block the current thread while a Task executes the structured concurrency code and makes the result available to the blocking thread, e.g. using a reference type.
This approach works in a specific context: i.e. the asynchronous job can (must) run on a different thread from the invoking one. This is also why this approach is dangerous: the solution is liable to deadlock if the invoking thread is part of task executor’s thread pool.
What I’m looking for is a mechanism by which a synchronous context can execute such an async job synchronously (immediately) if the current synchronous context is already isolated to a (serial) executor.
Ideally, if there is a current (serial) executor, there ought to be a way to take an async block and run it synchronously & receive the result, all without the use of locks or blocking the current thread.
Something like:
extension SerialExecutor {
public func runSynchronously<R, E: Error>(_ operation: @Sendable () async throws(E) -> R) throws(E) -> R
}
This would be particularly important in cases where Swift and C APIs interact, e.g.:
- Swift synchronous context →
- Swift Task →
- run Swift asynchronous code →
- make a C API call →
- C delegates back into Swift, e.g. via a function pointer →
- Swift implementation of the C function pointer wants to use structured concurrency →
- Swift asynchronous code relies on the existing Task/executor context
In this example, a C library could get executed from a Swift Task, thus existing within the execution context of a structured-concurrency-capable executor, the library may delegate back into Swift context where the author may want to continue to rely on this existing structured concurrency context to implement the result.
Theoretically, I see no reason why this shouldn’t be possible within e.g. a well defined custom SerialExecutor, but we lack the API to execute async code directly within a sync context, even where that context has the instruments in place to execute directly.
Is there any way to facilitate this currently? Perhaps we can unsafeBitCast the async away? What do we need to put in place before executing such async code directly from a sync context?