20 comments

[ 4.1 ms ] story [ 37.2 ms ] thread
One 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.
(comment deleted)
I loved the idea of Swift adopting actors however the implementation seems shoehorned. I wanted something more like Akka or QP/C++...
Concurrency 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)

@dang I think it's important that "fucking" remains in the title
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.

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.

I 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.

I still feel like Swift 5 (5.2?) was the sweet spot. Right now there are just way too many keywords, making C++ look easy again.

Similarly, I find Combine / GCD code far easier to write and read and understand, and the semantics are better than structured concurrency. I have plenty of production Combine code in use by (hundreds of) millions of people and it hasn't needed to be touched in years.

> 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).

This 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.
(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.)
Not 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/ . There seem to be at least 15 of them listed on 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"
But how would you do what in Rust's Tokio is a `spawn_blocking` in Swift?
Does 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?

I 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.

Good stuff. Would appreciate a section on bridging pre-async (system) libraries.
This 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.