Watch Out for Counterintuitive Implicit Actor-Isolation
I ran into some unexpected runtime crashes recently while testing an app on iOS 18 compiled under Swift 6 language mode, and the root causes ended up being the perils of using
@unchecked Sendable
in combination with some counterintuitive compiler behavior with implicit actor isolation.[…]
What occurred to me instead was to find a way to use locking mechanisms to synchronize access to the
static var
mutable property. What happened next led me down a path to some code that (A) compiled without warnings or errors but (B) crashed hard at runtime due to implicit actor isolation assertion failures.[…]
It turns out that the implicit Main Actor isolation is getting introduced by
MyApp
[…] Therefore thatinit()
method is isolated to the Main Actor. But ourLogging.sink
member is not isolated to the Main Actor. It’s implicitlynonisolated
, so why is the compiler inferring Main Actor isolation for the block we pass to it?
What’s happening here is the compiler is reasoning “this closure is not Sendable so it couldn’t possibly change isolation from where it was formed and therefore its body must be MainActor too” but your unchecked type allows this invariant to be violated. This kind of thing comes up a lot in many forms, and it’s hard to debug…
Mutex is a potential solution but requires iOS 18. He also shows how to protect the sink with a non-global actor.
Previously:
- Swift Vision: Improving the Approachability of Data-Race Safety
- Problematic Swift Concurrency Patterns
- Swift Concurrency and Objective-C
- Unwanted Swift Concurrency Checking
- Swift 6
- Swift Proposal: Synchronous Mutual Exclusion Lock
- Noncopyable Generics Walkthrough
- Swift Proposal: Noncopyable Structs and Enums