I am migrating to Swift 6 and get an error while Swift 5 compiled it.
Sending 'newElement' risks causing data races
Why is this and how do I fix it ?
actor AsyncStack<Element> {
private var storage = [Element]()
private var awaiters = [CheckedContinuation<Element,Error>]()
/// Push a new element onto the stack
/// - Parameter newElement: The element to push
/// - Returns: Void
public func push(_ newElement: Element) async -> Void {
if !awaiters.isEmpty {
let awaiter = awaiters.removeFirst()
awaiter.resume(returning: newElement) // ERROR in this line
} else {
storage.insert(newElement, at: 0)
}
}
/// Pop the element at the top of the stack or wait until an element becomes available
/// - Returns: The popped element
public func popOrWait() async throws -> Element {
if let element = storage.popLast() {
return element
}
return try await withCheckedThrowingContinuation { continuation in
awaiters.append(continuation)
}
}
}
In push
, you are essentially giving newElement
away to some other concurrency context, and newElement
is now shared between the context where it came from, and the context that it is sent to. Obviously, this is not safe if Element
is not Sendable
. Imagine a scenario like this:
// in Actor1...
await aStack.push(self.someNonSendableClass)
// in Actor2...
self.someNonSendableClass = try await aStack.popOrWait()
// Now the instance of someNonSendableClass is shared between the two actors!
You can either mark the newElement
parameter as sending
public func push(_ newElement: sending Element)
This essentially moves the check to the caller's side. When calling push
with a non-sendable Element
type, the compiler must be able to deduce that the caller will never access newElement
again, and therefore it will not be shared between two concurrency contexts.
Or you can force Element
to be Sendable
:
actor AsyncStack<Element: Sendable>