为什么withTaskGroup-work在Actor内部并行运行?

问题描述 投票:0回答:2

我知道,作为某些同步过程的一部分,参与者内部的所有并行工作都会以某种方式更改为串行工作。我们可以看到,应该并行完成的 async let 工作在 Actor1 中按顺序完成,很可能是由于 Actor 的内部同步。但是,尽管 AnActor 内部同步,但 withTaskGroup 工作仍并行运行,但为什么呢?)

编辑:同时,我想说,我理解在使用 await 时从 Actor 内部调用时同步是如何工作的,但我不明白同步是如何在 Actor 内部工作的,以调用异步并行演员内部的任务。

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().task {
                //await AnActor().performAsyncTasks() // uncomment this alternately and run
                //await Actor1().performAsyncTasks() // uncomment this alternately  and run
            }
        }
    }
}

actor Actor1 {
    
    func performAsyncTasks() async {
        async let _ = asyncTasks1() // this not running in parallel
        async let _ = asyncTasks2() // this not running in parallel
    }
    
    func asyncTasks1() async {
        for i in 1...10_000_0 {
            print("In Task 1: \(i)")
        }
    }
    
    func asyncTasks2() async {
        for i in 1...10_000_0 {
            print("In Task 2: \(i)")
        }
    }
}  // the printed text are in series with Task 1 and Task 2 in console

actor AnActor {
    var value = 0

    func performAsyncTasks() async {
        value = await withTaskGroup(of: Int.self) { group in
            group.addTask { // this running in parallel, why?!
                var value1 = 0
                for _ in 1...10_000 {
                    print("Task1")
                    value1 += 1
                }
                return value1
            }

            group.addTask { // this running in parallel, why?!
                var value2 = 0
                for _ in 1...10_000 {
                    value2 += 1
                    print("Task2")
                }
                return value2
            }

            return await group.reduce(0, +)
        }

        print(value)
    }
}  // the printed text are mixed with Task 1 and Task 2 in console
ios swift async-await concurrency
2个回答
1
投票

考虑你的第一个例子:

actor Actor1 {
    func performAsyncTasks() async {
        async let _ = asyncTasks1() // this not running in parallel
        async let _ = asyncTasks2() // this not running in parallel
    }
    
    func asyncTasks1() async {
        for i in 1...10_000_0 {
            print("In Task 1: \(i)")
        }
    }
    
    func asyncTasks2() async {
        for i in 1...10_000_0 {
            print("In Task 2: \(i)")
        }
    }
}

你说:

我们可以看到,应该并行完成的

async let
工作在
Actor1

中按顺序完成

是的,一般

async let
可以让例程同时运行。但是,在这种情况下,这种情况不会在这里发生,因为这两个函数都被隔离到同一个 actor,并且没有
await
悬挂点。


有人说:

如果

async let
let _ = await asyncTasks1()

的简写方式

事实并非如此。请参阅SE-0317

如果你想看到并行执行,你可以使用

async let
。你只需要把它们从当前的演员身上去掉就可以了。在这种情况下,请使用
nonisolated async
函数,如 SE-0338:

中所述
import os.log

actor Foo {
    private let poi = OSSignposter(subsystem: "Test", category: .pointsOfInterest)

    func bar() async throws {
        async let value1 = first()              // this IS running in concurrently
        async let value2 = second()             // this IS running in concurrently
        let total = try await value1 + value2
        print(total)
    }
}

private extension Foo {
    func first() async throws -> Int {
        let state = poi.beginInterval(#function, id: poi.makeSignpostID())
        defer { poi.endInterval(#function, state) }

        return try await inefficientCount()
    }

    func second() async throws -> Int {
        let state = poi.beginInterval(#function, id: poi.makeSignpostID())
        defer { poi.endInterval(#function, state) }

        return try await inefficientCount()
    }

    // This is `nonisolated` & `async` to get this off this actor. See SE-0338.
    // Also, routines doing slow calculations should periodically `yield` and check for cancelation.

    nonisolated func inefficientCount() async throws -> Int {
        var value = 0
        for i in 1...1_000_000_000 {
            if i.isMultiple(of: 10_000_000) {
                try Task.checkCancellation()
                await Task.yield()
            }
            value += 1
        }
        return value
    }
}

如果我在 Instruments 中对其进行分析,我可以看到它们并行运行:


1
投票

您所看到的行为是由于您正在执行的异步任务(当该任务绑定到

Actor
时)的性质造成的。

iOS 底层的线程执行模型不允许抢占。也就是说,CPU 永远不会从任务中“被夺走”。当一个任务放弃 CPU 时,其他任务就有机会开始执行。

此代码:

for _ in 1...10_000 {
    print("Task1")
    value1 += 1
}

CPU 限制 - 在

for
循环完成之前,任何其他任务都没有机会在 Actor 上运行。

Async/Await 通常用于有异步操作的地方;例如,网络操作。

如果我们对您的功能进行小改动:

func asyncTasks1() async {
    for i in 1...10_000_0 {
        print("In Task 1: \(i)")
        try? await Task.sleep(nanoseconds: 100)
    }
}
    
func asyncTasks2() async {
    for i in 1...10_000_0 {
        print("In Task 2: \(i)")
        try? await Task.sleep(nanoseconds: 100)
    }
}

您将看到两个函数的输出混合在一起,因为每个函数在每个

print
之后都会放弃 CPU,从而允许参与者执行另一个函数,直到 it 放弃 CPU。

现在,至于为什么您会看到

withTaskGroup
的不同行为 - 这是因为任务组未绑定到
Actor
,即使您在绑定到
Actor
的函数中创建它。一个任务组可以使用多个线程来执行任务。这是它的主要功能,允许一系列独立的操作在全部完成(或取消)时通过简单的集合点来执行。

如果删除添加的

await sleep
并对任务组代码进行小更改:

func performAsyncTasks() async {
    let a1=Actor1()
    value = await withTaskGroup(of: Int.self) { group in
    group.addTask { // this running in parallel, why?!
        var value1 = 0
        await a1.asyncTasks1()
        return value1
    }

    group.addTask { // this running in parallel, why?!
        var value2 = 0
        await a1.asyncTasks2()
        return value2
    }
    return await group.reduce(0, +)
}

您现在将看到两个循环按顺序完成,因为它们绑定到

Actor1
实例。

© www.soinside.com 2019 - 2024. All rights reserved.