Is `sending` allowed in AsyncSequences?

Is it possible to use sending on the return type of an AsyncSequence's next(isolation:) function?

I am trying to create an async sequence "forwarding" non-sendable values. These non-sendable values are created by the asyncsequence and can therefore be sent. For example:

actor CustomSequence: AsyncSequence, AsyncIteratorProtocol {
  init() {}

  nonisolated func makeAsyncIterator() -> CustomSequence {
    self
  }

  func next(isolation actor: isolated (any Actor)?) async throws(Never) -> sending Reference? {
    await computeNext()
  }

  nonisolated(unsafe) func next() async throws -> sending Reference? {
    await next(isolation: nil)
  }
}

private extension CustomSequence {
  func computeNext() async -> sending Reference? {
    Reference()
  }
}

final class Reference {}

The following throws an error within the next(isolation:) function saying: Sending 'self.computeNext' risks causing data races. Interestingly if I slightly modify next(isolation:) to:

func next(isolation actor: isolated (any Actor)?) async throws(Never) -> sending Reference? {
  let result = await computeNext()
  return result
}

Then I get a compiler error: Pattern that the region based isolation checker does not understand how to check. Please file a bug.

The compiler also emits a note with this error:

Error: Sending 'self.computeNext' risks causing data races
  Note: 'self'-isolated 'self.computeNext' cannot be a 'sending' result.
  'self'-isolated uses may race with caller uses

I'm not sure what this means to be honest, but FWIW your code compiles if you make your computeNext function nonisolated.

i've outlined my current best theory as to what's going on in such cases here. though i've not had 'official' confirmation from the domain experts, i am fairly confident that, as of now, the issue here doesn't have to do with AsyncSequence, but rather that any actor-isolated function of this form:

func returnSending() -> sending NonSendable { ... }

will effectively not return a sending value. instead the value is seemingly merged into the actor's region, so cannot be passed through as a sending parameter elsewhere.

as @ole accurately points out – a potential workaround is to ensure that the function producing the sending value is not actor-isolated.

2 Likes

Thank you @ole and @jamieQ. This is useful!

The whole idea with having an actor as a sequence is to have some kind of protected state at generation time. Therefore having a non-isolated computeNext() is not a good solution. In any case, it is good to know what are the current problems with actor and sending and therefore pivot to use other kind of concurrent state protection such as Mutexes.

depending on your needs, you may be able to adapt your code to build within the current constraints. does the construction of the non-Sendable elements of the sequence depend on other actor-isolated non-Sendable state? is so, then yes you may need another mechanism. but if not, perhaps you can achieve this via something like the following:

// make this either a global or static function
nonisolated func computeNext(for sequence: CustomSequence) async -> sending Reference? {
  let sendableState = await sequence.getSendableState() // pull out state that may be mutable but can be sent across isolation boundaries
  return Reference(sendableState) // construct the next non-Sendable element from it
}

that is, if you can separate some Sendable actor-protected state from the non-Sendable bits, then you might be able to use what you have with minor modifications.

1 Like