7 ms·
Approachable Swift Concurrency
- tombert 10mo agoI don't know a ton about Swift, but it does feel like for a lot of apps (especially outside of the gaming and video encoding world), you can almost treat CPU power as infinite and exclusively focus on reducing latency. Obviously I'm not saying you throw out big O notation or stop benchmarking, but it does seem like eliminating an extra network call from your pipeline is likely to have a much higher ROI than nearly any amount of CPU optimization has; people forget how unbelievably slow the network actually is compared to CPU cache and even system memory. I think the advent of these async-first frameworks and languages like Node.js and Vert.x and Tokio is sort of the industry acknowledgement of this. We all learn all these fun CPU optimization tricks in school, and it's all for not because anything we do in CPU land is probably going to be undone by a lazy engineer making superfluous calls to postgres.
- postsantum 10mo agoI worked on a resource-intensive android app for some years and it had a good perfomannce boost after implementing parallelization. But mostly for old shitty devices On latest phones it's barely noticiable
- Yoric 10mo agoThe answer to that would very much be: "it depends". Yes, of course, network I/O > local I/O > most things you'll do on your CPU. But regardless, the answer is always to measure performance (through benchmarking or telemetry), find your bottlenecks, then act upon them. I recall a case in Firefox in which we were bitten by a O(n^2) algorithm running at startup, where n was the number of tabs to restore, another in which several threads were fighting each other to load components of Firefox and ended up hammering the I/O subsystem, but also cases of executable too large, data not fitting in the CPU cache, Windows requiring a disk access to normalize paths, etc.
- tombert 10mo agoSure, I will admit I was a bit hyperbolic here. Obviously sometimes you need to do a CPU optimization, and I certainly do not think you should ignore big O for anything. It just feels like 90+% of the time my “optimizing” boils down to figuring out how to batch a SQL or reduce a call to Redis or something.
- wat10000 10mo agoSome of this is because you’re leaning on the system to be fast. A simple async call does a lot of stuff for you. If it was implemented by people who treated CPU power as if it was infinite, it would slow you down a lot. Since it was carefully built to be fast, you can write your stuff in a straightforward manner. (This isn’t a criticism. I work in lower levels of the stack, and I consider a big part of the job to be making it so people working higher up have to think about this stuff as little as possible. I solve these problems so they can solve the user’s problem.) It’s also very context dependent. If your code is on the critical path for animations, it’s not too hard to be too slow. Especially since standards are higher. You’re now expected to draw a frame in 8ms on many devices. You could write some straightforward code that decodes JSON to extract some base64 to decompress a zip to retrieve a JPEG and completely blow out your 8ms if you manage to forget about caching that and end up doing it every frame.
- tombert 10mo agoYeah, fair. I never found poll/select/epoll or the Java NIO Selector to be terribly hard to use, but even those are fairly high-level compared to how these things are implemented in the kernel.
- wat10000 10mo agoRight, and consider how many transformations happen to the data between the network call and the screen. In a modern app it's likely coming in as raw bytes, going through a JSON decoder (possibly with a detour through a native string type), likely getting marshaled into hash tables and arrays before being shoved into more specific model types, then pass that data along to a fully Unicode-aware text renderer that does high quality vector graphics.... There's a lot in there that could be incredibly slow. But since it's not, we can write a few lines of code to make all of this happen and not worry about optimization.
- seanalltogether 10mo agoOne of the things that really took me a long time to map in my head correctly is that in theory async/await should NOT be the same as spinning up a new thread (across most languages). It's just suspending that closure on the current thread and coming back around to it on the next loop of that existing thread. It makes certain data reads and writes safe in a way that multithreading doesn't. However, as noted in article, it is possible to eject a task onto a different thread and then deal with data access across those boundaries. But that is an enhancement to the model, not the default.
- jen20 10mo agoEDIT: Seems like newer versions of Xcode change the Swift language defaults here, but that is just the IDE, not the language (and Swift Package Manager does not appear to do the same!) I'd argue the default is that work _does_ move across system threads, and single-threaded async/await is the uncommon case. Whether async "tasks" move across system threads is a property of the executor - by default C#, Swift and Go (though without the explicit syntax) all have work-stealing executors that _do_ move work between threads. In Rust, you typically are more explicit about that choice, since you construct the executor in your "own" [1] code and can make certain optimizations such as not making futures Send if you build a single threaded one, again depending on the constraints of the executor. You can see this in action in Swift with this kind of program: import Foundation for i in 1...100 { Task { let originalThread = Thread.current try? await Task.sleep(for: Duration.seconds(1)) if Thread.current != originalThread { print("Task \(i) moved from \(originalThread) to \(Thread.current)") } } } RunLoop.main.run() Note to run it as-is you have to use a version of Swift < 6.0, which has prevented Thread.current being exposed in asynchronous context. [1]: I'm counting the output of a macro here as your "own" code.
- deleted 10mo ago[deleted]
- MORPHOICES 10mo ago[dead]
- mrkeen 10mo agoShare xor mutate, that's really all there is
- ragnese 10mo agoTalk about trivializing complexity... The idea that making things immutable somehow fixes concurrency issues always made me chuckle. I remember reading and watching Rich Hickey talking about Clojure's persistent objects and thinking: Okay, that's great- another thread can't change the data that my thread has because I'll just be using the old copy and they'll have a new, different copy. But now my two threads are working with different versions of reality... that's STILL a logic bug in many cases. That's not to say it doesn't help at all, but it's EXTREMELY far from "share xor mutate" solving all concurrency issues/complexity. Sometimes data needs to be synchronized between different actors. There's no avoiding that. Sometimes devs don't notice it because they use a SQL database as the centralized synchronizer, but the complexity is still there once you start seeing the effect of your DB's transaction level (e.g., repeatable_read vs read_committed, etc).
- mrkeen 10mo agoIt's not that shared-xor-mutate magically solves everything, it's that shared-and-mutate magically breaks everything. Same thing with goto and pointers. Goto kills structured programming and pointers kill memory safety. We're doing fine without both. Use transactions when you want to synchronise between threads. If your language doesn't have transactions, it probably can't because it already handed out shared mutation, and now it's too late to put the genie in the bottle. > This, we realized, is just part and parcel of an optimistic TM system that does in-place writes. [1] https://joeduffyblog.com/2010/01/03/a-brief-retrospective-on-transactional-memory/ https://joeduffyblog.com/2010/01/03/a-brief-retrospective-on...
- 10mo ago
- halfmatthalfcat 10mo agoI loved the idea of Swift adopting actors however the implementation seems shoehorned. I wanted something more like Akka or QP/C++...
- jen20 10mo ago> I wanted something more like Akka https://github.com/apple/swift-distributed-actors https://github.com/apple/swift-distributed-actors is more like Akka, but with better guarantees from the underlying platform because of the first-class nature of actors.
- troupo 10mo ago> the implementation seems shoehorned. Because it's extremely hard to retrofit actors (or, really, any type of concurrency and/or parallelism) onto a language not explicitly designed to support it from scratch.
- Someone 10mo agoI feel the reverse. I can see one can claim Swift has everything but the kitchen sink, but its actors, to me, don’t look shoehorned in. Reading https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors https://docs.swift.org/swift-book/documentation/the-swift-pr..., their first example is: actor TemperatureLogger { let label: String var measurements: [Int] private(set) var max: Int init(label: String, measurement: Int) { self.label = label self.measurements = [measurement] self.max = measurement } } Here, the ‘actor’ keyword provides a strong hint that this defines an actor. The code to call an actor in Swift also is clean, and clearly signals “this is an async call” by using await: await logger.max I know Akka is a library, and one cannot expect all library code to look as nice as code that has actual support from the language, but the simplest Akka example seems to be something like this (from https://doc.akka.io/libraries/akka-core/current/typed/actors.html https://doc.akka.io/libraries/akka-core/current/typed/actors...): object HelloWorld { final case class Greet(whom: String, replyTo: ActorRef[Greeted]) final case class Greeted(whom: String, from: ActorRef[Greet]) def apply(): Behavior[Greet] = Behaviors.receive { (context, message) => context.log.info("Hello {}!", message.whom) message.replyTo ! Greeted(message.whom, context.self) Behaviors.same } } I have no idea how naive readers of that would easily infer that’s an actor. I also would not have much idea about how to use this (and I _do_ have experience writing scala; that is not the blocker). And that gets worse when you look at Akka http (https://doc.akka.io/libraries/akka-http/current/index.html https://doc.akka.io/libraries/akka-http/current/index.html). I have debugged code using it, but still find it hard to figure out where it has suspension points. You may claim that’s because Akka http isn’t good code, but I think the point still stands that Akka allows writing code that doesn’t make it obvious what is an actor.
- scottmf 10mo agoConcurrency issues aside, I've been working on a greenfield iOS project recently and I've really been enjoying much of Swift's syntax. I’ve also been experimenting with Go on a separate project and keep running into the opposite feeling — a lot of relatively common code (fetching/decoding) seems to look so visually messy. E.g., I find this Swift example from the article to be very clean: func fetchUser(id: Int) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } And in Go (roughly similar semantics) func fetchUser(ctx context.Context, client *http.Client, id int) (User, error) { req, err := http.NewRequestWithContext( ctx, http.MethodGet, fmt.Sprintf("https://api.example.com/users/%d", id), nil, ) if err != nil { return User{}, err } resp, err := client.Do(req) if err != nil { return User{}, err } defer resp.Body.Close() var u User if err := json.NewDecoder(resp.Body).Decode(&u); err != nil { return User{}, err } return u, nil } I understand why it's more verbose (a lot of things are more explicit by design), but it's still hard not to prefer the cleaner Swift example. The success path is just three straightforward lines in Swift. While the verbosity of Go effectively buries the key steps in the surrounding boilerplate. This isn't to pick on Go or say Swift is a better language in practice — and certainly not in the same domains — but I do wish there were a strongly typed, compiled language with the maturity/performance of e.g. Go/Rust and a syntax a bit closer to Swift (or at least closer to how Swift feels in simple demos, or the honeymoon phase)
- tidwall 10mo agoOr this. func fetchUser(id int) (user User, err error) { resp, err := http.Get(fmt.Sprintf("https://api.example.com/users/%d", id)) if err != nil { return user, err } defer resp.Body.Close() return user, json.NewDecoder(resp.Body).Decode(&user) }
- jtbaker 10mo agoI'm conflicted about the implicit named returns using this pattern in go. It's definitely tidier but I feel like the control flow is harder to follow: "I never defined `user` how can I return it?". Also those variables are returned even if you don't explicitly return them, which feels a little unintuitive.
- Invictus0 10mo ago@dang I think it's important that "fucking" remains in the title
- JKCalhoun 10mo ago(It certainly makes it easier to find the topic some time after when going back to search for it on HN.)
- mojuba 10mo agoIt's a good article but I think you need to start explaining structured concurrency from the very core of it: why it exists in the first place. The design goal of structured concurrency is to have a safe way of using all available CPU cores on the device/computer. Modern mobile phones can have 4, 6, even 8 cores. If you don't get a decent grasp of how concurrency works and how to use it properly, your app code will be limited to 1 or 1.5 cores at most which is not a crime but a shame really. That's where it all starts. You want to execute things in parallel but also want to ensure data integrity. If the compiler doesn't like something, it means a design flaw and/or misconception of structured concurrency, not "oh I forgot @MainActor". Swift 6.2 is quite decent at its job already, I should say the transition from 5 to 6 was maybe a bit rushed and wasn't very smooth. But I'm happy with where Swift is today, it's an amazing, very concise and expressive language that allows you to be as minimalist as you like, and a pretty elegant concurrency paradigm as a big bonus. I wish it was better known outside of the Apple ecosystem because it fully deserves to be a loved, general purpose mainstream language alongside Python and others.
- ragnese 10mo ago> It's a good article but I think you need to start explaining structured concurrency from the very core of it: why it exists in the first place. I disagree. Not every single article or essay needs to start from kindergarten and walk us up through quantum theory. It's okay to set a minimum required background and write to that. As a seasoned dev, every time I have to dive into a new language or framework, I'll often want to read about styles and best practices that the community is coalescing around. I promise there is no shortage at all of articles about Swift concurrency aimed at junior devs for whom their iOS app is the very first real programming project they've ever done. I'm not saying that level of article/essay shouldn't exist. I'm just saying there's more than enough. I almost NEVER find articles that are targeting the "I'm a newbie to this language/framework, but not to programming" audience.
- jdgoesmarching 10mo ago> I promise there is no shortage at all of articles about Swift concurrency aimed at junior devs for whom their iOS app is the very first real programming project they've ever done. You’d be surprised. Modern Swift concurrency is relatively new and the market for Swift devs is small. Finding good explainers on basic Swift concepts isn’t always easy. I’m extremely grateful to the handful of Swift bloggers who regularly share quality content.
- isodev 10mo agoI really don't know why Apple decided to substitute terms like "actor" and "task" with their own custom semantics. Was the goal to make it so complicated that devs would run out of spoons if they try to learn other languages? And after all this "fucking approachable swift concurrency", at the end of the day, one still ends up with a program that can deadlock (because of resources waiting for each other) or exhaust available threads and deadlock. Also, the overload of keywords and language syntax around this feature is mind blowing... and keywords change meaning depending on compiler flags so you can never know what a code snippet really does unless it's part of a project. None of the safeties promised by Swift 6 are worth the burnout that would come with trying to keep all this crap in one's mind.
- hn-acct 10mo agoDo people actually believe that there are too many keywords? I’ve never met a dev irl that says this but I see it regurgitated on every post about Swift. Most of the new keywords are for library writers and not iOS devs. Preventing deadlock wasn’t a goal of concurrency. Like all options - there are trade offs. You can still used gcd.
- isodev 10mo ago> Do people actually believe that there are too many keywords? Yes they do. Just imagine seeing the following in a single file/function: Sendable, @unchecked Sendable, @Sendable, sending, and nonsending, @conccurent, async, @escaping, weak, Task, MainActor. For comparison, Rust has 59 keywords in total. Swift has 203 (?!), Elixir has 15, Go has 25, Python has 38. > You can still used gcd. Not if you want to use anything of concurrency, because they're not made to work together.
- travisgriggs 10mo ago> Instead of callbacks, you write code that looks sequential [but isn’t] (bracketed statement added by me to make the implied explicit) This sums up my (personal, I guess) beef with coroutines in general. I have dabbled with them since different experiments were tried in C many moons ago. I find that programming can be hard. Computers are very pedantic about how they get things done. And it pays for me to be explicit and intentional about how computation happens. The illusory nature of async/await coroutines that makes it seem as if code continues procedurally demos well for simple cases, but often grows difficult to reason about (for me).
- jesuslop 10mo agoThat is the price you pay. If you refuse to pay you are left to express a potentially complex state machine in terms of a flat state-transition table, so you have a huge python cases statement saying on event x do this and on event y do that. That obscures evident state-chart sequentiality, alternatives or loops (the stuff visible in the good old flow-charts) that otherwise could be mapped in their natural language constructs. But yes, is not honest flow. Is a tradeoff.
- jshier 10mo ago> looks sequential [but isn’t] This is just wrong. It looks sequential and it is! What the original author means is that it looks synchronous but isn't. But of course that's not really true either, given the use of the await keyword, but that can be explained by the brief learning curve. Swift concurrency may use coroutines as an implementation detail, but it doesn't expose most of the complexity of that model, or exposes it in different ways.
- lukeh 10mo agoSequential doesn’t mean reentrancy safe, something which has bitten me a few times in Swift concurrency.
- ChrisMarshallNY 10mo agoThis looks like it's well-written and approachable. I'll need to spend more time reviewing it, but, at first scan, it looks like it's nicely done.
- dang 10mo ago(We don't have a problem with profanity in general but in this case I think it's distracting so I've de-fuckinged the title above. It's still in the sitename for those who care.)
- cobbal 10mo agoNot saying if the title is good or bad, but just to provide context: there's a tradition of these style of apple-language explainers / cheat sheets titled in this pattern. First I'm aware of is https://fuckingblocksyntax.com/ https://fuckingblocksyntax.com/ . There seem to be at least 15 of them listed on https://fuckingsyntaxsite.com/ https://fuckingsyntaxsite.com/ , and probably more with different swears. It's a genre of titles in the same vein as "considered harmful" or "falsehoods programmers believe about"
- incanus77 10mo agoMy recollection was the block syntax one being first as well, back when that was added to Objective-C. It was arcane enough to have to look up all the time. They also have this variant, though: http://goshdarnblocksyntax.com/ http://goshdarnblocksyntax.com/
- sebastianconcpt 10mo agoBut how would you do what in Rust's Tokio is a `spawn_blocking` in Swift?
- cyberax 10mo agoDoes it do any refcounting optimizations? Are there any reference counting optimizations like biased counting? One big problem with Python multithreading is that atomic RCs are expensive, so you often don't get as much performance from multiple threads as you expect. But in Swift it's possible to avoid atomics in most cases, I think?
- ilumanty 10mo agoGood stuff. Would appreciate a section on bridging pre-async (system) libraries.
- andrekandre 10mo agoif you want to go into a long discussion/deep-dive into swift concurrency vs gcd and threads etc i'd recommend this thread on swift.org, it was very illuminating for me personally (and really fun to read) https://forums.swift.org/t/is-concurrent-now-the-standard-tool-for-shifting-expensive-synchronous-work-off-the-main-actor/ https://forums.swift.org/t/is-concurrent-now-the-standard-to...
- ballpug 10mo ago[dead]
- sambishop 10mo agoThis stops a little too short to be entirely useful imo. It's missing three very important concepts to make someone fully productive with swift concurrency: 1. sending. Using this keyword liberally will save you from the more heavyweight options like actors and Sendable. 2. isolated parameters. Inheriting the isolation of the caller is critical for functional style programming. 3. Dynamic isolation in general. Sometimes `assumeIsolated` is all you need. The fact that it recommends you pass this document to an agent without including these concepts almost guarantees the LLM is going to program itself into a corner.