I do agree that this is a surprising and pernicious pitfall — not just with Swift, but with all languages that support async / await (or anything coroutine-shaped, really) and also support shared mutable state in any form. While Swift concurrency is a vast improvement over the state of the art, the existence of the difficult-to-spot partial task (i.e. the stretch of control flow until the next await
) has serious downsides for our ability to reason about state and control flow.
I wrote about my concerns over this pitfall (and also here) during the reviews of the concurrency features. I still wonder if we might do anything about it.
As @ktoso noted above, non-reentrant actors could partially solve this problem…but with serious deadlock and performance downsides. Beyond those downsides, would they in fact completely solve the issue? It seems to me that the problem here is not only with actors, but with anywhere an await
appears.
Two other thoughts:
-
Are there useful warnings the compiler could emit? Could we, for example, warn if there are modifications to potentially shared state on both sides of an
await
within a function? Does the compiler have enough information to emit such a warning?If so, there could be a kind of explicit “commit work” operation that compiles to a noop (!), but lets the compiler know that the programmer believes they have left shared state in a good state and therefore a subsequent
await
is acceptable. -
Might we consider Eiffel-like invariant checks?
For those unfamiliar, the basic idea is that a type has function(s) that verify that all the class’s invariants are correct, and the compile automatically adds a call to the appropriate invariant check function(s) every time a public method returns — or before every
await
. The function is only a check; in optimized builds, it does not run.Those willing to add invariant checks to their types would presumably be able to catch misreasoning about
await
sooner.