Returning a 'self'-isolated value as a 'sending' result risks causing data races

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.

1 Like

I've also had a fair bit of trouble using sending with a 6.0 compiler, especially with closures. However, there have been a lot of improvements with 6.1, and I'm happy to report that this code compiles just fine with a 6.1 toolchain!

1 Like

That's great news, thank you !