swiftxcodeasync-awaitactorswift6

Error when compiling AsyncStack<> with Swift 6


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)
    }
  }
}

Solution

  • 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>