Swift concurrency, AsyncSequence and the Main event loop

Swift concurrency has been with us a little while now, as well as some of the supporting types around it such as AsyncSequence. Generally, I really enjoy the ergonomics of the API, and I'm looking forward to seeing it progress, but there's still something I'm not sure I understand fully and that's the best practices for interaction with Swift concurrency and the main event loop.

The Pre Concurrency Eco-System

Many components that exist in the Swift eco-system, particularly the Apple eco-system, are pre-concurrency. They broadcast events on the MainActor via Combine or NotificationCenter. Programmers could assume some level of synchronisation based on that fact. It was clear that if a notification or Publisher element was received on the MainActor, you could update your UI in the current event loop and there would be no context switching, and the UI wouldn't be at risk of becoming stale.

Bridging To Swift Concurrency

However, when we bridge these types of APIs to Swift Concurrency types, we lose these guarantees. Swift Concurrency forcefully redirects asynchronous traffic from the MainActor (or any Actor/GAIT) on to the default executor. If you wish to keep async calls on the MainActor, you can annotate the functions/closures you control with a specific actor, and you can even annotate a function with @_inheritsConcurrencyContext.

But even with these affordances, as soon as your async function calls into one of the system async libraries (AsyncSequence for example), you've lost control. You'll probably find yourself on the cooperative thread pool. You risk burdening the system with lots of context switching, or worse, you get synchronisation issues when your MainActor call takes the long way round via the cooperative thread pool and routines gets interleaved in an unexpected order.

Question

What's the current community recommended way of performing observation between two types that exist on the same Actor/GAIT where FIFO ordering is a requirement and context switching is not an option?

On Apple platforms, it seems like the best way today is Combine or even NotificationCenter. But these exist firmly in the 'pre-concurrency' old world. There's a pitch for Observable types which seems mindful of the interaction with the main EventLoop, but this seems to address only key paths to properties.

It would be great, and would seem intuitive and ergonomic, to be able to use a long running Task and for await message in stream { ... } syntax for this same Actor to same Actor message passing. Is this a direction under consideration?

2 Likes