Actor Reentrancy in Swift
When you start learning about actors in Swift, you’ll find that explanations will always contain something along the lines of “Actors protect shared mutable state by making sure the actor only does one thing at a time”. As a single sentence summary of actors, this is great but it misses an important nuance. While it’s true that actors do only one thing at a time, they don’t always execute function calls atomically.
[…]
However, when we introduce an async function that has a suspension point the actor will not sit around and wait for the suspension point to resume. Instead, the actor will grab the next message in its “mailbox” and start making progress on that instead. When the thing we were awaiting returns, the actor will continue working on our original function.
[…]
Things become trickier when you try and make your actor into a serial queue that runs async tasks. In a future post I’d like to dig into why that’s so tricky and explore possible solutions.
I think something like an async queue should be included in the concurrency library. The
AsyncStream
boilerplate is repeated everywhere for this sort of pattern, and we should make it easier to write because it’s a pattern that a lot of people need when order has to be guaranteed.
what’s worse:
- undetectable actor reenteancy bugs that freeze the process
- data race bugs that crash the app
can’t decide. hate both. one was not a big deal until recently in Swift
When most people first learn about actors, I think they expect it to work this way, ie, that each call completes, and the next begins, like a class with a serial queue. So I don’t think there is anything fundamentally wrong with the idea. Of course, what you open your door to are reentrances which can deadlock. Apple chose their evil, namely races, over the alternative, deadlocking. I find races harder to debug in general.
It is pretty clear that the reentrancy issue of Swift “actors” exists specifically to avoid the chance of deadlock, which an atomic, async, actor has.
I can’t say whether that is better or worse in practice, I’d say probably worse. Maybe it would have been better if an actor wouldn’t be allowed to issue async calls, and hence have real-actor-like atomic behaviour guaranteed. I’m pretty sure a ton of people are going to shoot themselves in the foot over the reentrancy issue.
Seems you can write a Swift macro to serialize execution of an async func. Trick is to embed a queue in the func, generating the appropriate return values etc.
Since the introduction of Actors, the Swift community is highly focused on them. I very rarely see Stuff like “Here is this cool new API and what you can do with it” anymore but only “How do I avoid data races and how does this Actor stuff actually work”. It is sad.
I took a shot at building an async-compatible lock.
Great for dealing with actor reentrancy. It’s kinda hard to use right now, becuase it cannot be built with the compiler in Xcode 16b4. I’d still love to hear what you think, even just about the concept.
Previously:
5 Comments RSS · Twitter · Mastodon
Summarizing, there's two issues that folks encounter with actors as they initially assume they'll fix all their problems and… won't.
1. The order that calls to the actor start is not guaranteed, even if those calls were made serially (i.e. from the main thread). This is an issue in general with Swift concurrency but becomes much more salient for many of the things folks think of using an actor to model. Swift 6 makes some strides towards making this easier to solve but it doesn't look like there's a simple, straightforward standard library facility for this yet.
2. An actor doesn't magically make your concurrent logic stop being concurrent. This is the reentrancy issue and at some level it's something you do to yourself. If all the implementation of your actor doesn't contain a single `await` you're not going to have this problem.
Reality being what it is your actor will often need to contain `async` logic. In that case the important thing to remember is that your actor's state should be valid at every yield point (`await`) and be able to resume from there no matter what other stuff happened in between.
It's not necessarily an easy problem to fix but I've found that isolating the concurrent logic to its minimal expression and moving the synchronous logic into methods that can be unit tested for validity before and after should offer enough peace of mind and keep actors quite a bit more useful than the alternative.
Swift keeps “solving” problems I don’t have, while making the problems I DO have worse with every release.
I can’t recall the last time I had a concurrency bug in an iOS app by simply following the pattern of sticking to the main thread, offloading long work to a background queue and always calling back on the main queue.
The problems I encounter daily with Xcode and Swift are getting worse: compiler crashes, the debugger being useless or not working, bugs in the standard libraries, bugs in the language, horrible compilation speed.
Swift is downright one of the worst compiler toolchains ever: in 25 years I’ve never used such a buggy toolchain and that includes early versions of PHP and .net.
Apple’s engineers are too busy scratching their own itches unfortunately.
Objective C++ is a nicer language than Objective C that doesn’t have the problems of Swift: stable & fast compiler that doesn’t crash. Use queues (GCD) to avoid all the Actor trouble.
> Fucking C++ monsters taking all the joy out of app development.
Yeah, completely agreed. First everyone slobbered over JavaScript and how breezy it was, no don't worry about types, worry about shipping!
Now we've got every tech influencer on yt and twitter praising type systems like they're some new phenomenon and the next big thing. Uhhhh.... earth to the react bros, this has been a thing for decades, you just chose to ignore it!
I don't mind the type safety in dart, but increasingly if everything is going to go type safe, I'd just as soon use C# than swift/rust/whatever.