Swift 5.10 produces a data race warning when looping over an `AsyncStream` on a global actor

Hey,

I'm currently trying to set up a Realm database observation that's not isolated to the Main Actor. This is proving to be quite a challenge (at least for me) because I've been running into tons of Concurrency warnings, most of which I don't quite understand. The context of this question revolves around Realm, but the underlying problem/topic is all about Swift Concurrency and actors, so I hope this forum is the appropriate place to ask.

To start, I have created a global actor BackgroundActor:

@globalActor public actor BackgroundActor: GlobalActor {
  public static let shared = BackgroundActor()
}

I use this to open/initialize a realm instance that's not isolated to the MainActor but to this BackgroundActor:

let realm = try await Realm(actor: BackgroundActor.shared)

Then, I built a small wrapper around RealmCollection.observe() (which only offers a closure based observation), that is isolated to that global BackgroundActor and returns an AsyncThrowingStream.

extension RealmCollection where Element: ObjectBase {
  @BackgroundActor func observe() -> AsyncThrowingStream<Void, Error> {
    .init { continuation in
      let token = observe { change in
        switch change {
        case .initial, .update:
          continuation.yield()
        case let .error(error):
          continuation.finish(throwing: error)
        }
      }

      continuation.onTermination = { _ in
        token.invalidate()
      }
    }
  }
}

With this, I want to observe some database queries, like this:

// Class is isolated
@BackgroundActor public class MyIsolatedClass {
  let realm: Realm
  init() { /* ... */ } // Initialize realm

  func observeDatabase() async throws {
    let results = realm.objects(ThoughtDTO.self).where { /* ... */ }
    for try await _ in results.observe() {
      print("Something changed, results object is now updated")
    }
  }
}

I use a Void yielding AsyncStream, because the Realm objects themselves are not Sendable and I could not get it to work without a multitude of Concurrency warnings, so I figured I'd just send "empty signals" to know when the data has changed (in the example above, results will always be updated by Realm, so I just need to know when something changed).

There are some ways to send Realm objects between actors through some thread safe references but that seems strange, since I don't believe I'm ever actually leaving the @BackgroundActor (thought I'm likely wrong).

The problem with the above example is the following warning I get on the for try await loop:

Warning: Passing argument of non-sendable type 'inout AsyncStream.Iterator' outside of global actor 'BackgroundActor'-isolated context may introduce data races

This warning appears when using Swift 5.10 (in Xcode 15.3 Beta 1). Though I have no idea why it appears. Why is the iterator outside of the @BackgroundActor and how can I fix this?


To illustrate it better, I have a minimal reproducing example here:

@BackgroundActor public func observe() -> AsyncStream<Void> {
  .init { continuation in
    continuation.yield()
  }
}

@BackgroundActor public class MyIsolatedClass {
  func doStuff() async throws {
    for await _ in observe() { // Warning: Passing argument of non-sendable type 'inout AsyncStream<Void>.Iterator' outside of global actor 'BackgroundActor'-isolated context may introduce data races
      print("Yielded")
    }
  }
}

My understanding around global actors is not too great, I'd love to understand what is happening here any why this might be a problem.

I'd appreciate any help :slightly_smiling_face:

1 Like

There's an explanation of the problem in [5.10][Concurrency] Suppress diagnostics about non-`Sendable` async iterator arguments in implicit calls to `next()`. by hborla · Pull Request #70750 · apple/swift · GitHub, which also suppresses this warning for AsyncStream.Iterator and friends in Swift 5.10.

There's a deeper explanation of the problem + a proper solution under review now in SE-0421: Generalize effect polymorphism for AsyncSequence and AsyncIteratorProtocol

4 Likes

Thank you!