DispatchQueue SerialExecutor isolation check is not consistent on Darwin

from Swift 5.9 Darwin Environment supports DIspatchQueue as SerialExecutor
However DispatchQueue's isolation check in Concurrency is inconsistent.

I have a DispatchActor like below.

@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, visionOS 1.0, *)
actor DispatchActor {
    
    let queue: _DispatchSerialExecutorQueue
    
    init(queue: DispatchQueue) {
        if let serial = queue as? _DispatchSerialExecutorQueue {
            self.queue = serial
        } else {
            let delegate = DispatchQueue(label: queue.label, target: queue)
            if let serial = delegate as? _DispatchSerialExecutorQueue {
                self.queue = serial
            } else {
                fatalError()
            }
        }
        
    }
    
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        queue.asUnownedSerialExecutor()
    }
}

This code evalute success

            let mainDispatch = DispatchActor(queue: .main)
            await withUnsafeContinuation{ continuation in
                mainDispatch.queue.async {
                    continuation.resume()
                    // passes
                    mainDispatch.preconditionIsolated()
                    // passes
                    MainActor.shared.preconditionIsolated()
                    RunLoop.main.perform {
                        // passess
                        MainActor.shared.preconditionIsolated()
                    }
                }
            }

This make sense since in usual Darwin environment MainActor is MainRunLoop and DispatchQueue.main

However, below the code evaluate failure.

            let privateActor = DispatchActor(queue: DispatchWorkloop(label: "private"))
            await withUnsafeContinuation{ continuation in
                privateActor.queue.async {
                    continuation.resume()
                    // assert!
                    privateActor.preconditionIsolated()
                }
            }
            let privateActor = DispatchActor(queue: DispatchQueue(label: "private"))
            await withUnsafeContinuation{ continuation in
                privateActor.queue.async {
                    continuation.resume()
                    // assert!
                    privateActor.preconditionIsolated()
                }
            }

I wonder if this behavior is intended, and if it is why?

I tried this and don't actually reproduce the issue. Can you confirm what OS/SDK you're building and running on?

There is some integration in Dispatch that sets the apropriate executor tracking information.

My OS/SDK is like below.

execution environments:
iOS 17.2 iPhon15 pro Simulator(target),
M1 air MacOS sonoma 14.3 Beta(23D5051b) (target and building)

Xcode Version 15.2 (15C500b)
xcrun swift -version:
swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx14.0

Okey so this actually is "just" that queue.async{} doesn't set any executor tracking and these assertions are expected and "as designed" tbh. The precondition APIs work with Swift's concurrency model -- so they check tasks and the thread local executor; if there is none -- and there is none here because the queue.async just never sets any such information -- Swift has no idea about it, and thus asserts.

The main actor happens to work because the fallback on "we assume main actor is main thread", which does not require dispatch to do anything. (This assumption is actually true only on 80% of cases as there can be runtimes where the thread serving the main actor and "main queue" is NOT the "main thread", but this is a rare case).

For such assertions to work there must be a TASK that is enqueued on a custom executor that is such queue, and in that run task the precondition is then used and would work.

We can improve on this, and here's a draft: [SerialExecutor] PoC: Allow a SerialExecutor to check its own tracking for isolation checks by ktoso ยท Pull Request #71172 ยท apple/swift ยท GitHub

however rolling this out will need both a swift evolution proposal, and special work for backdeployment.

Keep an eye for evolution proposals and that patch, but as it stands today this is as-designed in 5.10.

5 Likes

A solution for this is under review now: SE-0424: Custom isolation checking for `SerialExecutor`

4 Likes