async/await引用捕获本地对象是否总是安全或理论上不安全?

huangapple go评论65阅读模式
英文:

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:

async/await引用捕获本地对象是否总是安全或理论上不安全?

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

以下是翻译好的内容:

观察几点:

  1. 你提出了以下问题:

    为什么异步/等待对可变对象的捕获是安全的?

    请澄清一下,User 不是“可变对象”。

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

    它是一个不可变对象。一个“可变对象”是指具有可以改变的属性的对象。在这种情况下,我们正在处理一个变量,例如,您可以将一个不可变对象替换为另一个。

    注意,您后一个示例的编译器错误说:

    在并发执行的代码中引用捕获的变量 'user'

    问题出在捕获的 var,而不是对象是否可变。

  2. 让我们考虑您的第一个示例:

    let _ = await fetchId(for: user)
    

    因此,这里传递了一个不可变对象 User 的副本给 fetchId。它还会暂停当前任务的执行,直到 fetchId 返回。

    为什么这段代码不会像下面的代码一样产生错误?

    因为此示例使用了 await,它会暂停执行。这会阻止对此本地变量的任何其他交互,直到 fetchId 返回。

    在另一个示例中,您使用了 async let (SE-0317),它允许执行继续,允许与捕获的 user 变量进行其他交互。再次强调,问题不在于 User 对象是否可变,而在于相对于 user 变量的竞态。

  3. 现在,您在其他地方提出了以下问题:

    主要问题是“异步/等待对可变对象的捕获总是安全的还是从理论上不安全的?”

    让我们想象一下,您实际上正在处理一个可变对象。在那种情况下,除非可变对象是 Sendable,否则它不是绝对安全的。请参考 WWDC 2021 的视频 使用 Swift 操作员保护可变状态 和 2022 年的 使用 Swift 并发消除数据竞争

英文:

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:

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

答案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."

huangapple
  • 本文由 发表于 2023年5月10日 17:17:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76216756.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定