英文:
Async/await reference-capturing of local objects is always safe or theoretically unsafe?
问题
async
/await
capturing of mutable objects is safe because Swift's concurrency model is designed to handle potential issues like concurrent access to mutable objects. In your code, the line let _ = await fetchId(for: user)
doesn't produce errors because async/await
ensures that the access to the user
variable is serialized, meaning only one asynchronous task can access it at a time, preventing data races.
The reason async let name = fetchName(for: user)
generates an error is that you're explicitly creating a new asynchronous task that captures the user
variable, potentially allowing concurrent access to it. To avoid this, Swift requires you to mark the property with @MainActor
or use Task.withUnsafeContinuation
when working with mutable state concurrently in an asynchronous context.
So, the compiler does protect potential unsafe asynchronous code, but it does so in a way that allows safe concurrency by default and requires explicit annotations when needed.
英文:
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
.
答案1
得分: 2
以下是翻译好的内容:
观察几点:
-
你提出了以下问题:
为什么异步/等待对可变对象的捕获是安全的?
请澄清一下,
User
不是“可变对象”。struct User: Decodable { let id: UUID let name: String let age: Int }
它是一个不可变对象。一个“可变对象”是指具有可以改变的属性的对象。在这种情况下,我们正在处理一个变量,例如,您可以将一个不可变对象替换为另一个。
注意,您后一个示例的编译器错误说:
在并发执行的代码中引用捕获的变量 'user'
问题出在捕获的
var
,而不是对象是否可变。 -
让我们考虑您的第一个示例:
let _ = await fetchId(for: user)
因此,这里传递了一个不可变对象
User
的副本给fetchId
。它还会暂停当前任务的执行,直到fetchId
返回。为什么这段代码不会像下面的代码一样产生错误?
因为此示例使用了
await
,它会暂停执行。这会阻止对此本地变量的任何其他交互,直到fetchId
返回。在另一个示例中,您使用了
async let
(SE-0317),它允许执行继续,允许与捕获的user
变量进行其他交互。再次强调,问题不在于User
对象是否可变,而在于相对于user
变量的竞态。 -
现在,您在其他地方提出了以下问题:
主要问题是“异步/等待对可变对象的捕获总是安全的还是从理论上不安全的?”
让我们想象一下,您实际上正在处理一个可变对象。在那种情况下,除非可变对象是
Sendable
,否则它不是绝对安全的。请参考 WWDC 2021 的视频 使用 Swift 操作员保护可变状态 和 2022 年的 使用 Swift 并发消除数据竞争。
英文:
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:
>
swift
>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 untilfetchId
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 untilfetchId
returns.In the other example, you have an
async let
(SE-0317), however, which lets execution continue, allowing other interaction with the captureduser
variable. Again, the question is not the (im)mutability of theUser
object, but the race with respect to theuser
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.
答案2
得分: 0
The first call to user
has no reason to generate an error, while when you say it is "obvious" to see an error on the second call, it is not for the reason you think but because you made a mistake in the code.
The error would come if you try to capture a variable that can change from outside the Task
. The variable user
is declared inside it, so there is no concurrent code that can change its value.
The error you see is because you used async
instead of await
.
Just replace: async let name = fetchName(for: user)
With: let name = await fetchName(for: user)
Exactly like you did when you called fetchId()
.
Edit
However, if you need to use async let name = fetchName(for: user)
, this means that you are downloading the name in parallel with the rest of the code. Which means: in the meantime, the variable user
can change. When you tried to define user
as a let
, the error disappeared.
But in the first call, you are using await
, which means that the code will not continue until you have finished fetching the ID. So, the code is blocked until the await
line is completed: there is no way that the variable user
can change. That's why there's no error.
Apple explains it better here: in their example, "Each photo downloads completely before the next one starts downloading". "To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant."
英文:
The first call to user
has no reason to generate an error, while when you say it is "obvious" to see an error on the second call, it is not for the reason you think but because you made a mistake in the code.
The error would come if you try to capture a variable that can change from outside the Task
. The variable user
is declared inside it, so there is no concurrent code that can change its value.
The error you see is because you used async
instead of await
.
Just replace: async let name = fetchName(for: user)
With: let name = await fetchName(for: user)
Exactly like you did when you called fetchId()
.
Edit
However, if you need to use async let name = fetchName(for: user)
, this means that you are downloading the name in parallel with the rest of the code. Which means: in the meantime, the variable user
can change. When you tried to define user
as a let
, the error disappeared.
But in the first call, you are using await
, which means that the code will not continue until you have finished fetching the ID. So, the code is blocked until the await
line is completed: there is no way that the variable user
can change. That's why there's no error.
Apple explains it better here: in their example, "Each photo downloads completely before the next one starts downloading". "To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant."
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论