import Dispatch
actor Example {
final class DispatchQueueExecutor: SerialExecutor {
private let queue: DispatchQueue
init(queue: DispatchQueue) {
self.queue = queue
}
func enqueue(_ job: UnownedJob) {
self.queue.async {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
private let executor: DispatchQueueExecutor
init(queue: DispatchQueue) {
self.executor = DispatchQueueExecutor(queue: queue)
}
func bridge() {
print("Sadly, I never get my moment.")
}
nonisolated var unownedExecutor: UnownedSerialExecutor {
executor.asUnownedSerialExecutor()
}
}
let queue = DispatchQueue(label: "Example")
let example = Example(queue: queue)
queue.sync {
example.assumeIsolated {
$0.bridge()
}
}
Fatal error: Incorrect actor executor assumption; Expected same executor as Example.
lldb confirms that it is in fact running in the correct Dispatch queue when assumeIsolated is invoked (and that, if I instead spawn up an intermediary Task and await into the actor that way, that the actor is using the given Dispatch queue for its execution).
Implementing isSameExclusiveExecutionContext makes no difference (whether or not I change it to use UnownedSerialExecutor(complexEquality:)).
This seems like it can only be a bug?
Is there a workaround, to allow me to efficiently (and synchronously) invoke the actor's methods from inside the Dispatch queue?
I'm using Swift 6 (swiftlang-6.0.0.3.300 clang-1600.0.20.10).
3 Likes
dehesa
(Marcos Sánchez-Dehesa)
2
Just got to the same crash today. As far as I understand from @ktoso pitch and its subsequent proposal, this issue will be solved by implementing checkIsolated(). However that function seems to only be used by runtimes from macOS 15+. Is this right @ktoso ? Any way to use assumeIsolated(_:) in OSes lower than 15?
My current executor looks similar to @wadetregaskis with slight differences since I need to support macOS 14:
public import Dispatch
public extension DispatchQueue {
final class Executor: SerialExecutor {
public let queue: DispatchQueue
public init(label: ReverseDNS, qos: DispatchQoS, autoreleaseFrequency: AutoreleaseFrequency = .inherit) {
self.queue = DispatchQueue(label: label.rawValue, qos: qos, attributes: [], autoreleaseFrequency: autoreleaseFrequency, target: nil)
}
}
}
public extension DispatchQueue.Executor {
@inlinable func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(consume job)
let unownedSerialExecutor = self.asUnownedSerialExecutor()
#if compiler(>=6.0)
if #available(macOS 15, *) {
let unownedTaskExecutor = self.asUnownedTaskExecutor()
queue.async {
unownedJob.runSynchronously(isolatedTo: unownedSerialExecutor, taskExecutor: unownedTaskExecutor)
}
} else {
queue.async {
unownedJob.runSynchronously(on: unownedSerialExecutor)
}
}
#else
queue.async {
unownedJob.runSynchronously(on: unownedSerialExecutor)
}
#endif
}
@inlinable func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
#if compiler(>=6.0)
@available(macOS 15, *)
extension DispatchQueue.Executor: TaskExecutor {
@inlinable public func asUnownedTaskExecutor() -> UnownedTaskExecutor {
UnownedTaskExecutor(ordinary: self)
}
@inlinable public func checkIsolated() {
dispatchPrecondition(condition: .onQueue(queue))
}
}
#endif
Is there any way to backdeploy TaskExecutors as well?
ktoso
(Konrad 'ktoso' Malawski 🐟🏴☠️)
3
There’s no way to backdeploy this feature without significant effort and tbh I don’t even know if possible at all. So, no.