Swift Concurrency: Waiting for Async Work
A common point of confusion in Swift Concurrency turns out to actually not be unique to Swift at all: “why can’t you synchronously wait for future async work?”
If you block a thread in your thread pool waiting, a core goes idle, but another one runs the work and unblocks it; not too awful.
What happens if you block all of them? Where does the work they’re waiting for run?
[…]
libdispatch picked “cap at ~64 threads”, which allows misdesigned code to “get away with it” sometimes, at a cost in memory and CPU overhead.
Swift follows the philosophy of “fail fast”: rather than let people write inefficient code that occasionally deadlocks in edge cases, it tries to make people aware of the problem up front.
SwiftUI gives us very few affordances for dealing with async state, while actors create lots of it.
So we are now aware of the problem up front, but that “painful rearchitecting” is mostly about dealing with Apple frameworks with little guidance on how to do it.
To solve a problem we never see. (I’m certain Apple sees it regularly at scale, but we don’t.)
[…]
I keep having the situation where I make an actor just to eventually find all of its state wrapped into a single
Mutex<State>
so that outside things can access it atomically. And I find myself wondering what the actor was supposed to be doing for me and I make it a class again.
It’s so easy to say that one should design their systems to be async, but then you run into APIs that require sync answers that you don’t control 🤷🏻♂️
I think “indeterminate” would be a better description than “long running”. As long as the locked code makes forward progress and doesn’t wait itself, it should be fine?
So the difficulty we run into is that trying to do it statically (by not providing any APIs to do so), we can’t distinguish between “it’s probably ok” and “not that one”.
But beyond that, I’ve personally been wrong about this. I broke booting iOS with a single spinlock (no priority donation) around three calls to look up the username for a uid, which happened once ever and then were cached. Should have been fine, I thought!
My answer to how I read this question is: blocking a thread on sync IO is safe, even if that’s the last available thread in the thread pool, because the IO has a thread to run on (the current one, since it is a sync call). Whereas blocking on async IO may require another thread be available for said async IO to run on.
NSFileCoordination
was … interesting … but one of my favorite bits of using it to write our file syncing thing was the zen of realizing there is no observable current filesystem state, only changes floating in the wind and how you respond as they drift past you.
I had 0 bugs
- added async
- added debounce
- I have undefined number of nondeterministic bugs now
Previously:
- Swift Vision: Improving the Approachability of Data-Race Safety
- Swift Concurrency Proposal Index
- Calling async Code Synchronously in Swift
- Actor Reentrancy in Swift
- Limiting Swift Concurrency’s Cooperative Pool
- Underused and Overused GCD Patterns
- libdispatch’s Unmet Promise
- iOS Optimization Tips
- Practical Concurrency: Some Rules
- When to Use dispatch_async()
- GCD Tips
- Grand Central Dispatch’s Achilles Heel