Hi everyone,
I'm trying to implement an async serial queue using Swift Concurrency. The queue can take async closures which optionally return a result and executes them in order.
I have problem with the return type T
of the closures : if I require T to be sendable then it compiles but I'd like to loosen that restriction to sending T
in order to be able to execute with non sendable results.
Here is the full implementation which doesn't compile :
public actor AsyncSerialQueue {
private let priority: TaskPriority?
private var tasks: [(run: @Sendable () async -> Void, cancel: @Sendable () -> Void)] = []
private var currentTask: Task<Void, any Error>?
public init(priority: TaskPriority? = nil) {
self.priority = priority
}
deinit {
for task in tasks {
task.cancel()
}
currentTask?.cancel()
}
public func execute<T>(_ operation: @escaping @Sendable () async throws -> sending T) async throws -> sending T {
// Following line produces the error : Returning a 'self'-isolated '@noescape @callee_guaranteed @substituted <τ_0_0> (@in_guaranteed CheckedContinuation<τ_0_0, any Error>) -> () for <T>' value as a 'sending' result risks causing data races
return try await withCheckedThrowingContinuation() { (continuation: CheckedContinuation<T, any Error>) in
let run = { @Sendable in
do {
let result = try await operation()
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
let cancel = { @Sendable in
continuation.resume(throwing: CancellationError())
}
tasks.append((run, cancel))
if currentTask == nil {
startTask()
}
}
}
private func startTask() {
if currentTask == nil && !tasks.isEmpty {
let run = tasks.removeFirst().run
currentTask = Task(priority: priority) { [weak self] in
await run()
try Task.checkCancellation()
await self?.finishTask()
}
}
}
private func finishTask() {
currentTask = nil
startTask()
}
public func cancel() {
for task in tasks {
task.cancel()
}
tasks.removeAll()
currentTask?.cancel()
currentTask = nil
}
}
As I said, changing func execute<T>(_ operation: @escaping @Sendable () async throws -> sending T) async throws -> sending T
to func execute<T: Sendable>(_ operation: @escaping @Sendable () async throws -> T) async throws -> T
makes it compile but that doesn't solve my problem, and most of all, I'd like to understand what I'm doing wrong here.
Thanks for your help and answers.