-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Description
Description
Sometimes I need to run async code synchronously, such as when an Apple framework API calls my closure (no async support) and requires a result from it, but the code required to retrieve that result is async. In an ideal world this wouldn't occur, but in typical macOS and iOS GUI programming it occurs quite often.
And conceptually this is trivial; indeed in prior paradigms like NSRunloop this sort of pattern was easy to implement, since runloops were re-entrant. There's no such API available to me for Swift Concurrency, as far as I'm aware.
I've almost got it working with Swift Concurrency, without runtime issues and compiler complaints, but I'm stuck on the last hurdle and I'm not sure it's not a compiler / stdlib bug.
Reproduction
import Dispatch
extension Task {
static func sync(_ code: () async throws(Failure) -> Success) throws(Failure) -> Success {
let semaphore = DispatchSemaphore(value: 0)
nonisolated(unsafe) var result: Result<Success, Failure>? = nil
withoutActuallyEscaping(code) {
let sendableCode = unsafeBitCast($0, to: (@Sendable () async throws -> Success).self)
_ = Task<Void, Never>.detached(priority: .userInitiated) { @Sendable () async -> Void in
do {
result = .success(try await sendableCode())
} catch {
result = .failure(error as! Failure)
}
semaphore.signal()
}
semaphore.wait()
}
return try result!.get()
}
}Expected behavior
Everything runs fine, life moves on.
The actual behaviour is that it just silently hangs at runtime, even though the code compiles without any complaints in Swift 6 mode. The code closure is never actually invoked; the Task's closure seems to just silently hang when trying to call it. There is no evidence of it anywhere in any thread's callstack (the thread running sync is stuck on the semaphore.wait()).
Yet if I mark the code parameter as @Sendable, it works fine. However, then I'm erroneously requiring code to be sendable and it causes design and implementation problems for the caller (mainly re. capture local state, which isn't and shouldn't have to be sendable because there's no concurrency or escaping involved).
My only hypothesis so far is that it's trying to force code to run on the same thread as sync is called on, thus a fairly pedestrian deadlock, but: I don't know why (the code closure isn't intentionally bound to any particular isolation context) and I don't understand how merely making the closure @Sendable somehow changes that. Thus why I'm wondering if this is actually expected behaviour in any sense, or rather a bug.
I tried in vain every possible attribute and modifier I could think of (lots of nonisolated stuff, @_inheritActorContext, etc), even stuff I didn't expect to work merely for completeness, and nothing has helped.
Environment
Xcode 15 beta 5.
swift-driver version: 1.113 Apple Swift version 6.0 (swiftlang-6.0.0.7.6 clang-1600.0.24.1)
Additional information
No response