AsyncStream and Actors

Something like that, if I recall correctly. I believe the idea was to have a program-wide total ordering of locks. Though my memory is fuzzy on whether that was enforced entirely at compile-time or if some aspect of it was at runtime (I seem to recall the use of the lock's address in memory for ordering, although I might be crossing my wires).

Many thanks Cory. So for the record, this is the correct way to use AsyncStream to pass data from a GCD thread to the swift concurrent world, is that correct ?

final class ClassSource: Sendable {

    private(set) lazy var frames: AsyncStream<Data> = {
            AsyncStream { (continuation: AsyncStream<Data>.Continuation) -> Void in
                self.frameContinuation = continuation
            }
        }()

    private var frameContinuation: AsyncStream<Data>.Continuation?
    let serialBackgroundQueue = DispatchQueue(label: "Network")

    // Data comes in here on a GCD background queue
    func receiveData(newData: Data) {
        self.frameContinuation?.yield(newData)
    }

    func listener() {
        serialBackgroundQueue.async {
            while ...
                self.receiveData(newData: data)
        }
    }
}

class Consumer { // Over in swift concurrency world
    @MainActor
    func readFrames(source: ClassSource) async {
        for await frame in source.frames {
            self.process(frame)
        }
    }
}

It would be great to add to the documentation for yield that it is safe to call it from any thread.

Perhaps the fact that AsyncStream.Continuation confirms to Sendable should be a sufficient clue, but for me, I still struggle with the name (I tell myself, "so what if I can send this to an async context?"), and lack trust for what Sendable gives me without specific direction. While I may be able to convince myself it could be safe to call this function from an async context, what tells me it is safe to call from any thread ? Is that universally true of any Sendable ?

I wonder if much of the confusion for struggling adopters like me comes from the documentation for Sendable which describes the protocol from the compiler's persepective, rather than from the programmer's perspective of what can they do with a Sendable data type or Sendable function and how they should think about it.

The opening line of the documentation for Sendable is "You can safely pass values of a sendable type from one concurrency domain to another", which sadly means very little to me (apologies). If it said something like "A Sendable data type may be safely used concurrently by multiple Tasks" and "A Sendable function may be safely called from any Thread or Task", then it would have helped me understand much more quickly.

Even more helpful would be a set of sanctioned examples in the documentation on the various strategies to convert an existing non Sendable data type into a Sendable, using 1) locks, 2) queues, 3) semaphores, etc. That topic deserves a full chapter alone in The Swift Programming Language. As the compiler rudely complains so many of our data types are not Sendable, the first question we ask ourself is "How do I make it Sendable?". I am afraid that without a strong reliable resource, many programmers will mark data types @unchecked Sendable and swift's great leap forward (sic) ends in tears...

3 Likes

I also think that at the moment the Swift documentation is lagging behind in explaining important concepts behind concurrency. But on the other hand, and as was mentioned a few times already, this is all work in progress, so that's understandable.

I recently started re-reading the evolution proposals starting from 0300-continuation.md, because there's so much important information tucked away in those :sweat_smile:

1 Like

Don't forget that lazy isn't thread-safe, so the code as written here is almost certainly unsafe. You should set up the async stream in init.

Otherwise, yes, yield is safe to call from wherever.

This is generally what Sendable requires: the interface to the type must be safe to call from any thread. How the type achieves that (CoW, locks, etc.) is up to the type.

But you need to compute a new stream for each client, same way notifications works, so a var setup in init won't do.