How async/await works internally in Swift
swiftrocks.com
swiftrocks.com
For example async required iOS 15.0. Why is this tied to the OS? Why can't they include newer runtimes be downloadable like Node / Java / .NET etc?
Other examples are from SwiftUI. For example the NavigationStack appears much more useful than the older NavigationView but that requires iOS 16. Which means that you can't support anything older than an iPhone X.
So in that kind of market, this approach does reduce support costs because you just pick a target iOS version to support and test with devices running that and you know newer devices will also just work.
I assume you're talking about the latest iOS version for their device, which may or may not be the latest iOS version released by Apple. I was running around with an iPhone 5 until 2019ish when apps stopped working altogether.
It’s not reasonable to compare native apps to browser support. Dropping support for an older version of iOS doesn’t cut users off, they can continue to use the more recent version of your app that supports their device.
It’s especially unreasonable to compare it to people supporting Internet Explorer for a decade. Microsoft halted all development for five years and people carried on using it much longer. Apple comes out with a new major version of iOS every year and nobody is using decade-old versions of iOS.
It works fine so I don’t see a need to get a new phone, except less and less apps are going to continue to work eventually
The other support costs it reduces is for Apple because their testing matrix for making runtime releases is drastically reduced. Which means the swift team runs more efficiently.
It’s annoying but for end users it’s even more valuable because apps are smaller (and more longevity for storage) and less memory is used.
TLDR: Apple has always run the languages this way and it works nicely for their ecosystem.
I've been using Swift since its inception, it's been years since I've been able to pause execution at a certain breakpoint and do "po object" and get something back that is not an error.
It is frustrating though, although in my case 40% of my DAU are already on iOS 17 (>3m devices) so it’s not the end of the world.
That could be fixed by just shipping an updated shared library to all phones similar to how Google play services works on Android, but I guess they figure if you're updating anyway you might as well just update the whole OS.
Having said that, I find Apple’s implementation exceptionally complex. Aiming for safety and catering to inexperienced devs, they’ve created something way too complex for less experienced devs to understand. In my personal opinion, they should have overlooked Actor safety and gone with a simpler model which is easily understandable, and rely on experienced devs to understand what is happening under the covers and program accordingly.
To use a cooking analogy, We live in a bizarro world where chefs are being asked to abandon cooking knives (they’re dangerous), and to use dozens of kitchen aids for safety. And the limitations this produces…
C# and Rust async models are based on top of state machines and cooperative yielding back to the executor. The difference between C# and Rust is a tradeoff of either the ability to just not think and use async/await naturally with really nice defaults but paying for those with heap allocations of state captured by continuations or dealing with the memory model explicitly which requires more effort but gives you fully configurable and/or deterministic behavior, that can (and usually does) achieve far lower overhead.
Java uses green (virtual) threads where the runtime can preempt (pause/suspend) them (keeping virtual thread stack in memory) and schedule the execution of a different green thread on top of the current physical one, not dissimilar to C#/Rust as they achieve so explicitly, where the next work item is executed by the threadpool once the current one yields.
Basically NSOperation scheduled to a non-concurrent operation queue, where the operation itself is made of async calls.
What would be the recommended approach for doing that with swift concurrency ?
One actor per queue, then one actor per operation ?
IMO I would recommend not interacting with async/await as much as possible and stick to dispatch queues you can reason about far more easily.
Swift async/await has worked excellently for me so far. The biggest issue is that most libraries aren't updated to use it (and sometimes couldn't be, because they require Custom Actor Executors, which weren't available until Swift 5.9).
I found the following video helpful to better understand Swift async/await: "Swift concurrency: Behind the scenes" https://developer.apple.com/videos/play/wwdc2021/10254
Swift concurrency is still in a transitory period, and with that comes some warnings about how you can mix it with legacy concurrency primitives. i.e. not holding a lock across Task boundaries.
However, it's fairly well documented. There's a talk 'Swift concurrency: Behind the scenes' [1], that goes into detail on this. View from around the 25 min mark.
I replaced some of my other async code with Combine, which I do really like now, it's proving itself to be pretty solid
I’ve written several async Swift apps and not hid deadlocks but I also tend to structure my data in ways that avoid shared rw access where possible.
The important quote:
> both Swift concurrency and Dispatch’s queues are serviced by same underlying pool of threads — Swift concurrency’s jobs just promise not to block on future work
What this means, is that if you're in a Swift concurrency context, and you dispatch_async work to a concurrent queue, then use a semaphore (or similar) to block on that work completing, then the thread pool implementation will not backfill the blocked thread. Crucially, this is true even if the code doing the semaphore hack is old ObjC code that used to work fine.
So if some older code you happen to be calling is doing something like:
func badIdea() {
let sem = DispatchSemaphore()
someConcurrentQueue.async {
doLongRunningThing(completion: { sem.signal() })
}
sem.wait()
}
and you happen to call `badIdea()` from all cores simultaneously, you'll deadlock.Now, under normal pre-Swift-Concurrency circumstances, GCD would spawn a new thread to handle the queue.async block, which would free the semaphore (this leads to thread explosion, but at least not deadlocks.) But if the call to `badIdea()` happens to be done by a Swift Concurrency Task, then the thread pool gets a hint saying "don't worry, this thread will never block on future work", so it doesn't spawn a new thread to handle the dispatch_async, and you're hosed.
How exposed you are to this issue depends on what kind of code you're calling (third party, even code written by Apple) that may be doing this semaphore hack. You don't have to do this semaphore hack yourself, for this to be a problem. You just have to call into poorly written framework code which may be doing this.
Now, the answer to this problem is that "nobody should write code that does this", which is absolutely true, but it also is the case that there's a lot of code which does it anyway. A lot of people run into this function-coloring issue (which existed before `async/await` was a thing, completion-based functions have the exact same problem) and find themselves painted into a corner where they need to be synchronous, but they need to call asynchronous code, and using a semaphore works, so they just do it and ship it. Swift Concurrency rather silently changes the contract here so that stuff that used to be "merely" a bad idea, is now a deadlock.
Most apps are not as intense as ours, they are web app equivalents that just do a few http calls and display form data. Our app is pretty intense with gpu background jobs, image queries, ai models running, local db modifications and network uploads occurring, which is way more of a concurrency stress test than most. It acts like a local only desktop app with some optional internet features.
It’s the observability blocking that is the worst part. If we could observe deadlock states then it wouldn't be as bad.
i'm pretty sure that's not the reason for the birth of Swift. Trying to access the 20th element from an array of 2 elements would cause a crash in almost all languages including Objective-C which Swift replaced.
It depends on which part of objective C you’re referring to: the C part, or the objective part. ObjC has NSArray, which has safe, bounds checked accessors. But ObjC is a strict superset of C, and C has very unsafe C-style arrays. In the latter, you definitely don’t always get a simple crash for accessing out of bounds… you get UB and buffer overflow exploits, same as C.
Like if there were no other languages before C++ or Obj-C, eh? What about C? Or Fortran? Or Ada? Speaking of Ada…
> Apple was one of the companies at the time that recognised the need for a safer, modern alternative to these languages.
People recognised need for safer languages way before Swift was created.
At least, I think it was Ada. Can't ask any more, he was born in '39, we scattered his ashes nearly a decade ago.
What Swift offers in safety is already available in Lisp, D, Ada, OCaml, Haskell, Delphi, among many others, for a few decades.