Search code examples
swiftasync-awaitconcurrency

Async/await reference-capturing of local objects is always safe or theoretically unsafe?


Why async/await capturing of mutable objects is safe? For example, the code with the async let capturing of mutable objets immediately produce an error:

See image example.

Here full code of example:

struct User: Decodable {
    let id: UUID
    let name: String
    let age: Int
}

func executeExample() {
    Task {
        var user = User(id: UUID(), name: "Taylor Swift", age: 26)
        let _ = await fetchId(for: user) // Why this code is not produce error, like code below?

        async let name = fetchName(for: user) // Reasons for this error obvious for me
        user = User(id: UUID(), name: "Taylor", age: 27)
        await print("Found \(name) name.")
    }
}


func fetchName(for user: User) async -> String {
    Thread.sleep(forTimeInterval: 5)
    print("Done fetchName")
    return user.name
}

func fetchId(for user: User) async -> UUID {
    Thread.sleep(forTimeInterval: 5)
    print("Done fetchId")
    return user.id
}

This piece of code async let name = fetchName(for: user) generates an error Reference to captured var 'user' in concurrently-executing code and that's ok, since I understand why it's happening.

Why doesn't this let _ = await fetchId(for: user) code produce errors? It seems to me that this asynchronous method could be a potential opportunity for writing unsafe code. For example, isn't there the potential concurrent access to a user variable from multiple asynchronous threads? And to avoid double concurrent access to the object we need to use actor's.

For example: at the let _ = await fetchId(for: user) functions suspension point, system give a chance to another async functions(threads) to execute, so another threads(async funcs) can do write and read access at the save time to user variable, which will produce errors! If it didn't work that way, then we wouldn't need to use actors anymore! So I have a question, why the compiler does not protect potential unsafe asynchronous code let _ = await fetchId(for: user) by producing this error Reference to captured var 'user' in concurrently-executing code.


Solution

  • A few observations:

    1. You asked:

      Why async/await capturing of mutable objects is safe?

      Just to clarify, but the User is not a “mutable object”.

      struct User: Decodable {
          let id: UUID
          let name: String
          let age: Int
      }
      

      It is an immutable object. A “mutable object” is one with with properties that can mutate. In this case, we are dealing with a variable, e.g., where you might replace, for example, one immutable object with another.

      Note, your compiler error for your latter example says:

      Reference to captured var 'user' in concurrently-executing code

      The problem is a captured var, not whether an object was mutable or not.

    2. Let us consider your first example:

      let _ = await fetchId(for: user)
      

      So, this is passing fetchId a copy of an immutable object, User. It also suspends execution for this current task until fetchId returns.

      Why this code is not produce error, like code below?

      Because this example has await, which suspends execution. That prevents any other interaction with this local variable until fetchId returns.

      In the other example, you have an async let (SE-0317), however, which lets execution continue, allowing other interaction with the captured user variable. Again, the question is not the (im)mutability of the User object, but the race with respect to the user variable.

    3. Now, elsewhere you ask:

      The main question is “Async/await capturing of mutable objects is always safe or theoretically unsafe?”

      Let us imagine that you actually were dealing with a mutable object. In that case, it is not inherently safe unless the mutable object is Sendable. See WWDC 2021’s video Protect mutable state with Swift actors and 2022’s Eliminate data races using Swift Concurrency.