Oh dear, yes this is another post from a seasoned developer struggling with the strict checking in Xcode 5.10 highlighting how everything they thought they knew and understood about swift concurrency was wrong (or should I say (unsafe)).
Here is one example. I have been using an AsyncStream to serialize reads coming from a classic Thread API, into a Task that awaits new frames of data. Switching on strict checking lights up my code in golden warnings, so I tried to use an actor to take control of things...
actor ActorSource {
private lazy var frames: AsyncStream<Data> = {
AsyncStream { (continuation: AsyncStream<Data>.Continuation) -> Void in
self.frameContinuation = continuation
}
}()
private var frameContinuation: AsyncStream<Data>.Continuation?
lazy private var frameIterator = frames.makeAsyncIterator()
func nextFrame() async -> Data? {
// Error: Cannot call mutating async function 'next()' on actor-isolated property 'frameIterator'
await self.frameIterator.next()
}
func receiveData(newData: Data) {
frameContinuation?.yield(newData)
}
}
I don't see why the compiler is unhappy with this. The only way I was able to compile this functionality into an actor was by declaring frames, frameContinuation and frameIterator as nonisolated(unsafe), which makes me rather uneasy. Even though that code compiles without complaint or golden warnings, I don't believe this is actually thread safe, as it seems to me the actor isolation is not enforced here.
If I revert the actor to a class, where I restrict the functions interacting with the AsyncStream to a global actor, it appears to me to be thread safe.
class ClassSource {
private lazy var frames: AsyncStream<Data> = {
AsyncStream { (continuation: AsyncStream<Data>.Continuation) -> Void in
self.frameContinuation = continuation
}
}()
private var frameContinuation: AsyncStream<Data>.Continuation?
lazy private var frameIterator = frames.makeAsyncIterator()
@MainActor
func nextFrame() async -> Data? {
await self.frameIterator.next()
}
func receiveData(newData: Data) {
Task { @MainActor in
frameContinuation?.yield(newData)
}
}
}
However reverting to a class lights up a series of other warnings, in other functions in the class where Tasks access self, about the class not being Sendable. If I tell the compiler I have dealt with the thread safety by declaring it Sendable, I get a warning about the AsyncStream frames being mutable. I can get rid of that warning by making it conform instead to @unchecked Sendable, but again I feel uneasy about what other compiler checks I may be losing out on.
So my questions are:
- Why can I not use an AsyncStreamIterator as a property of an actor ?
- What is the recommended pattern for using an AsyncStream safely between a classic GCD API and swift concurrency ?
As a side note, I have to say some of the terminology of swift concurrency really makes things difficult for me to reason about. Sendable is perhaps the worst example. I finally got it recently when I saw a post from HollyBora explain that Sendable actually means ThreadSafe. I wish it had been called that from the outset, it would have simplified my understanding tremendously. I am tempted to use a type alias, to help me think about my code that way.
I am having to rethink logic and structure throughout my apps as a result of the Swift 5.10 strict checking, which is immensely frustrating. I hope that the changes I am making are really actually needed to improve concurrent safety, but in some cases I wonder if I am being forced to jump through hoops to make changes that are not actually necessary...