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:
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
.
A few observations:
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.
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.
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.