I've read a number of pitches and looked at @ _unsafeInheritExecutor, but I'm still confused about the best way to solve a particular problem I'm having, both today and in Swift 6 when SE-0420 is available.
I am trying to predictably bridge AsyncSequence to RxSwift. For those unfamiliar, RxSwift has similar features to the asynchronous parts of Swift Concurrency; Observable is basically an AsyncSequence, if you ignore the concurrency aspects of AsyncSequence they're almost isomorphic.
By "predictably", I mean "the actor you start on in Swift Concurrency corresponds roughly to the thread you receive it on. In particular, if you call convertToObservable on MainActor, then call subscribe (the equivalent to a for-await in Rx), I want the subscription callback to be run on the main queue.
Here, then, is the implementation from the RxSwift library. It does not meet my requirements, because it uses an unstructured Task, and because the Observable is not isolated, the Task runs on a random thread.
func asObservable() -> Observable<Element> {
Observable.create { observer in
let task = Task {
do {
for try await value in self {
observer.onNext(value)
}
observer.onCompleted()
} catch {
observer.onError(error)
}
}
return Disposables.create { task.cancel() }
}
}
I seem to be able to make this function safe by adding @_unsafeInheritExecutor, but that requires me to mark the function as async. As an aside, I'm confused as to why an annotation that seems to promise that it always runs on the same actor needs to be awaited. Perhaps the point this annotation is making is that exeuctor inheritance is not the same as actor inheritance? I do not fully understand the implications, if that is the case.
On Swift 6
The above only has to work for a few months, so I think I'm fine with it being async. But when I try to use the tools in SE-0420, I immediately run into an issue. Here's a program that demonstrates it:
class FakeObservable {
func thingThatShouldInheritIsolation(
isolation: isolated (any Actor)? = #isolation
) {
isolation?.assertIsolated() // fine, and is mainactor
Task {
MainActor.assertIsolated() // no longer main actor
}
}
}
@MainActor
func ff() {
FakeObservable().thingThatShouldInheritIsolation()
}
await MainActor.run {
ff()
}
try! await Task.sleep(for: .seconds(3)) // wait long enough for the task to start in normal conditions
But what's exceptionally odd is that by capturing the isolation parameter in the Task, the assertion stops failing:
class FakeObservable {
func thingThatShouldInheritIsolation(
isolation: isolated (any Actor)? = #isolation
) {
isolation?.assertIsolated() // fine, and is mainactor
func internallyIsolated(
isolation: isolated (any Actor)?
) {
MainActor.assertIsolated()
}
Task {
MainActor.assertIsolated() // no longer main actor
internallyIsolated(isolation: isolation)
}
}
}
This does solve my problem, but it looks a lot like a compiler bug of some kind. Can anyone offer any insights into what's happening?