async/await:如何在@MainActor类中在后台线程上运行异步函数?

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

async/await: How do I run an async function within a @MainActor class on a background thread?

问题

以下是您要翻译的内容:

  1. Task.detached

    func getFoo() async -> Foo {
        await Task.detached {
            let foo = // ... some slow, synchronous, CPU-intensive operation ...
            return foo
        }.value
    }
    
  2. Continuation + GCD

    func getFoo() async -> Foo {
        await withCheckedContinuation { continuation in
            DispatchQueue.global().async {
                let foo = // ... some slow, synchronous, CPU-intensive operation ...
                continuation.resume(returning: foo)
            }
        }
    }
    

Arguments Against Task.detached

> [You] should not run long-running expensive, blocking operations in a concurrency context, regardless of if it's the main actor or not. Swift concurrency is a cooperative model, i.e. the functions that run in a concurrency context are expected to regularly suspend to give up control of their thread and give the runtime a chance to schedule other tasks on that thread.
>

Source

> Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.
>

Swift concurrency: Behind the scenes (WWDC21)

如果我理解这两句话的意思正确的话,不应该在Swift并发环境中执行长时间运行的昂贵的阻塞操作,无论是在主要actor中还是其他地方。 Swift并发是一种协作模型,即在并发上下文中运行的函数应定期挂起,以放弃对线程的控制权,并给运行时机会安排在该线程上调度其他任务。

Arguments Against Continuations + GCD

> One should avoid using DispatchQueue at all.
>

Source

> I would avoid introducing GCD, as that does not participate in the cooperative thread pool and Swift concurrency will not be able to reason about the system resources, avoid thread explosion, etc.
>

Source

因此,以上所有内容考虑后,运行async函数在后台线程上的正确方法是什么?

英文:

Let’s say I have an async function within a @MainActor class that performs some slow, synchronous, CPU-intensive operation:

@MainActor
class MyClass {
    // ...

    func getFoo() async -> Foo {
        let foo = // ... some slow, synchronous, CPU-intensive operation ...
        return foo
    }

    // ...
}

How can I make my function run on a background thread (so that I don’t block the main thread)? I’ve seen two different approaches but am not sure which is correct (if either) as I’ve seen arguments against both of them.

  1. Task.detached

    func getFoo() async -> Foo {
        await Task.detached {
            let foo = // ... some slow, synchronous, CPU-intensive operation ...
    		return foo
        }.value
    }
    
  2. Continuation + GCD

    func getFoo() async -> Foo {
        await withCheckedContinuation { continuation in
            DispatchQueue.global().async {
                let foo = // ... some slow, synchronous, CPU-intensive operation ...
                continuation.resume(returning: foo)
            }
        }
    }
    

Arguments Against Task.detached

> [You] should not run long-running expensive, blocking operations in a concurrency context, regardless of if it's the main actor or not. Swift concurrency is a cooperative model, i.e. the functions that run in a concurrency context are expected to regularly suspend to give up control of their thread and give the runtime a chance to schedule other tasks on that thread.
>

Source

> Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.
>

Swift concurrency: Behind the scenes (WWDC21)

If I’m understanding these two quotes correctly, slow, synchronous operations shouldn’t be done within Swift concurrency as they would block forward progress and that would violate the runtime contract. That being said, I would have assumed that the alternative would have been continuations + GCD, but I’ve also seen arguments against that.

Arguments Against Continuations + GCD

> One should avoid using DispatchQueue at all.
>

Source

> I would avoid introducing GCD, as that does not participate in the cooperative thread pool and Swift concurrency will not be able to reason about the system resources, avoid thread explosion, etc.
>

Source

So, all of that being said, what is the correct way of running an async func on a background thread?

答案1

得分: 2

Ideally, the long computation should periodically yield, then you can remain entirely within Swift concurrency.


You quote this comment:

> [You] should not run long-running expensive, blocking operations in a concurrency context, regardless of if it’s the main actor or not. Swift concurrency is a cooperative model, i.e. the functions that run in a concurrency context are expected to regularly suspend to give up control of their thread and give the runtime a chance to schedule other tasks on that thread.

We should note that two sentences later, they answer your question:

> You can do this by calling await Task.yield() periodically…

So, bottom line, if you have your own long-running routine, you should periodically yield and you can safely perform computationally intense calculations within Swift concurrency while upholding the contract to not impede forward progress.


This begs the question: What if the slow routine cannot be altered to periodically yield (or otherwise suspend in a cooperative manner)?

In answer to that, the proposal, SE-0296 - Async/await, says:

> Because potential suspension points can only appear at points explicitly marked within an asynchronous function, long computations can still block threads. This might happen when calling a synchronous function that just does a lot of work, or when encountering a particularly intense computational loop written directly in an asynchronous function. In either case, the thread cannot interleave code while these computations are running, which is usually the right choice for correctness, but can also become a scalability problem. Asynchronous programs that need to do intense computation should generally run it in a separate context. When that’s not feasible, there will be library facilities to artificially suspend and allow other operations to be interleaved.

So, the proposal is reiterating what was said above, that you should integrate the ability to “interleave code” (e.g., periodically yield within the long computations). If you do that, you can safely stay with Swift concurrency.

Re long computations that do not have that capability, the proposal suggests that one “run it in a separate context” (which it never defined). On the forum discussion for this proposal, someone asked for clarification on this point, but this was never directly answered.

However, in WWDC 2022’s Visualize and optimize Swift concurrency, Apple explicitly advises moving the blocking code out of the Swift concurrency system:

> Be sure to avoid blocking calls in tasks. … If you have code that needs to do these things, move that code outside of the concurrency thread pool – for example, by running it on a DispatchQueue – and bridge it to the concurrency world using continuations.

So, if you cannot periodic yield, to allow interleaving, move this code outside of the cooperative thread pool, e.g., a dispatch queue. Note that Swift concurrency will not be able to reason about other threads that might be tying up CPU cores, which theoretically can lead to an over-commit of CPU resources (one of the problems that the cooperative thread pool was designed to address), but at least it eliminates the deadlock risks and other concerns that are discussed in that video.

英文:

tl;dr

Ideally, the long computation should periodically yield, then you can remain entirely within Swift concurrency.


You quote this comment:

> [You] should not run long-running expensive, blocking operations in a concurrency context, regardless of if it’s the main actor or not. Swift concurrency is a cooperative model, i.e. the functions that run in a concurrency context are expected to regularly suspend to give up control of their thread and give the runtime a chance to schedule other tasks on that thread.

We should note that two sentences later, they answer your question:

> You can do this by calling await Task.yield() periodically…

So, bottom line, if you have your own long-running routine, you should periodically yield and you can safely perform computationally intense calculations within Swift concurrency while upholding the contract to not impede forward progress.


This begs the question: What if the slow routine cannot be altered to periodically yield (or otherwise suspend in a cooperative manner)?

In answer to that, the proposal, SE-0296 - Async/await, says:

> Because potential suspension points can only appear at points explicitly marked within an asynchronous function, long computations can still block threads. This might happen when calling a synchronous function that just does a lot of work, or when encountering a particularly intense computational loop written directly in an asynchronous function. In either case, the thread cannot interleave code while these computations are running, which is usually the right choice for correctness, but can also become a scalability problem. Asynchronous programs that need to do intense computation should generally run it in a separate context. When that’s not feasible, there will be library facilities to artificially suspend and allow other operations to be interleaved.

So, the proposal is reiterating what was said above, that you should integrate the ability to “interleave code” (e.g., periodically yield within the long computations). If you do that, you can safely stay with Swift concurrency.

Re long computations that do not have that capability, the proposal suggests that one “run it in a separate context” (which it never defined). On the forum discussion for this proposal, someone asked for clarification on this point, but this was never directly answered.

However, in WWDC 2022’s Visualize and optimize Swift concurrency, Apple explicitly advises moving the blocking code out of the Swift concurrency system:

> Be sure to avoid blocking calls in tasks. … If you have code that needs to do these things, move that code outside of the concurrency thread pool – for example, by running it on a DispatchQueue – and bridge it to the concurrency world using continuations.

So, if you cannot periodic yield, to allow interleaving, move this code outside of the cooperative thread pool, e.g., a dispatch queue. Note that Swift concurrency will not be able to reason about other threads that might be tying up CPU cores, which theoretically can lead to an over-commit of CPU resources (one of the problems that the cooperative thread pool was designed to address), but at least it eliminates the deadlock risks and other concerns that are discussed in that video.

答案2

得分: 1

根据你的第二段引用所说,

> 当你采用Swift并发时,确保在代码中继续保持这个契约非常重要,以便合作线程池可以实现最佳功能。

这里的“契约”指的是允许其他线程取得进展,你可以通过“放弃对线程的控制并让运行时有机会在该线程上安排其他任务”来实现,就像你的第一段引用所说的那样。

因此,这就是你应该做的。

将你想做的任务分解成非“长时间运行”的小任务。例如,你可以将 manipulateResult 和获取结果的函数都设为 async

func doSomethingExpensive() async -> Foo {
    let result = await getResult()
    let manipulatedResult = await manipulateResult(result)
    return manipulatedResult
}

你也可以将 getResultmanipulateResult 使用的函数设为 async,并await它们的返回值。示例代码:

func getResult() async -> Bar {
    let x = await subTask1()
    let y = await subTask2(x)
    return await subTask3(x, y)
}

func manipulateResult(_ result: Bar) async -> Foo {
    let x = await subTask4(result)
    let y = await subTask5(x)
    return y
}

每当你写下 await 时,你都有一个悬挂点,Task 可以“放弃对线程的控制”,让该线程上的其他任务取得进展。

你也可以通过等待 Task.yield() 来让其他任务有机会运行,但如果当前任务具有最高优先级,则这种方法将不起作用。

是的,如果你不想继承演员上下文,你应该使用 Task.detached

英文:

As your second quote says,

> As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.

The "contract" refers to allowing other threads to make progress, and you do that by "give up control of their thread and give the runtime a chance to schedule other tasks on that thread", as your first quote says.

So that's what you should do.

Break up the task you want to do into non-"long running" smaller pieces. For example, you can make manipulateResult and the function that gets you the result both async.

func doSomethingExpensive() async -> Foo {
    let result = await getResult()
    let manipulatedResult = await manipulateResult(result)
    return manipulatedResult
}

You can make the functions that getResult and manipulateResult uses async too, and await their return values. Illustrative code:

func getResult() async -> Bar {
    let x = await subTask1()
    let y = await subTask2(x)
    return await subTask3(x, y)
}

func manipulateResult(_ result: Bar) async -> Foo {
    let x = await subTask4(result)
    let y = await subTask5(x)
    return y
}

Every time you write await, you have a suspension point where the Task can "give up control of their thread" and allow other tasks on that thread to make progress.

You can also give other tasks a chance to run by awaiting Task.yield(), but that wouldn't work if the current task has the highest priority.

And yes, Task.detached is what you should use if you don't want to inherit the actor context.

huangapple
  • 本文由 发表于 2023年7月10日 12:40:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76650710.html
匿名

发表评论

匿名网友

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

确定