Bridging async > sync code in swift concurrency

To bridge sync code to async code we have Task { .. }
The other way around is needed as well to bridge legacy systems.

I don't see anything like this available, unless I'm missing something
I'm thinking something like this

func legacyCode() {
   let someResultFromAsyncWorld = BlockingTask { // new async code here }
   // old sync code here
}

Kotlin's equivalent is runBlocking { // async context here }

Would a simple semaphore be sufficient?

func BlockingTask<T>(_ body: @escaping () async throws -> T) throws -> T {
    let semaphore = DispatchSemaphore(value: 0)
    let box = Box<T>()
    Task {
        do {
            let result = try await body()
            box.result = .success(result)
        } catch {
            box.result = .failure(error)
        }
        semaphore.signal()
    }
    semaphore.wait()
    return try box.result!.get()
}

private final class Box<T> {
    var result: Result<T, Error>?
}

I tried the semaphore route & it worked…until it didn’t. It seems like Swift Concurrency creates only one thread per system processor core and once you have every core blocked waiting for your async work, your app is deadlocked.

what do you mean every core blocked; cores cannot be blocked, I can have 1000 threads if I want to, even though inefficient

The cooperative thread pool has many threads as there are cores. Ideally, they kernel-mode context switching of the threads is minimal, and all the work is done by user-mode context switching of Tasks, which is much much much faster.

For that to work though, it's imperative that your async code never blocks. If you need to call legacy code that blocks, then you should spin that off in a separate thread using all the existing techniques (e.g. a dispatch queue), and using a continuation to signal the completion of its work.

I'd highly recommend Swift concurrency: Behind the scenes from WWDC 2021

2 Likes

I don't follow. This is to bridge some legacy code, not a modus operandi for new async code.

@christopherweems said it will deadlock, which will only be the case if BlockingTask is called from a thread within that cooperative thread pool; which is not the intended usage.

How do you intend to enforce this?

Also, isn't that Task initializer creating a new task which will always use the cooperative thread pool? It's been a while and IDR when that thread pool is or isn't used, I need to re-watch that video xD

You can't enforce it, but that's okay for this case. Same way you cannot enforce people to always call semaphore.signal()

If you need to call legacy code that blocks..

This is for the other way around - when your old legacy blocking code wants to call new async stuff

If you know that you’ll never call your bridging code from the cooperative pool & mark it with @available(*, noasync) I think the approach would work.

1 Like

Doesn't compile for me, am I doing it wrong?

This is the feature I’m referencing, it’s only a marker to prevent you from calling it from an async context but it’s new to 5.7. You can leave it off assuming you’re already doing that checking yourself.