Approachable Swift Concurrency

44 points | by wrxd 2 hours ago

8 comments

  • scottmf 29 minutes ago

    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)

      tidwall 9 minutes ago

      Or 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)
          }
  • MORPHOICES an hour ago

    How do you actually learn concurrency without fooling yourself?

    Every time I think I “get” concurrency, a real bug proves otherwise.

    What finally helped wasn’t more theory, but forcing myself to answer basic questions:

    What can run at the same time here?

    What must be ordered?

    What happens if this suspends at the worst moment?

    A rough framework I use now:

    First understand the shape of execution (what overlaps)

    Then define ownership (who’s allowed to touch what)

    Only then worry about syntax or tools

    Still feels fragile.

    How do you know when your mental model is actually correct? Do you rely on tests, diagrams, or just scars over time?

      mrkeen 7 minutes ago

      Share xor mutate, that's really all there is

  • seanalltogether an hour ago

    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.

      jen20 3 minutes ago

      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.

  • halfmatthalfcat 33 minutes ago

    I loved the idea of Swift adopting actors however the implementation seems shoehorned. I wanted something more like Akka or QP/C++...