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 Task
s 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...