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

wtzytmuj  于 2023-06-25  发布在  iOS
关注(0)|答案(2)|浏览(68)

我理解所有在actor内部的并行工作在某种程度上变成了串行的,作为某种同步过程的一部分。我们可以看到,应该并行完成的async let-work在Actor 1中顺序完成,很可能是由于actor的内部同步。但是,withTaskGroup-work并行运行,尽管AnActor内部同步,但为什么?)
编辑:同时,我想说,当使用await时,我理解从actor内部外部调用同步是如何工作的,但我不理解在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
0h4hbjxa

0h4hbjxa1#

考虑你的第一个例子:

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可以让例程并发运行。但这里不会发生这种情况,因为这两个函数都是独立于同一个参与者的,并且没有await挂起点。
有人说:
如果async letlet _ = await asyncTasks1()的简写方式…
它不是。参见SE-0317
如果你想看到并行执行,你可以使用async let。你只需要使用非隔离函数:

actor Foo {
    func performAsyncTasks() async {
        async let value1 = asyncTasks1() // this IS running in parallel
        async let value2 = asyncTasks2() // this IS running in parallel
        let total = await value1 + value2
        print(total)
    }

    nonisolated func asyncTasks1() async -> Int {
        await poi.interval(name: #function) {
            var value = 0
            for _ in 1...1_000_000_000 {
                value += 1
            }
            return value
        }
    }

    nonisolated func asyncTasks2() async -> Int {
        await poi.interval(name: #function) {
            var value = 0
            for _ in 1...1_000_000_000 {
                value += 1
            }
            return value
        }
    }
}

如果我在仪器中分析它,我可以看到它们并行运行:

顺便说一下,上面使用了以下POI实用函数:

import os.log

let poi = OSLog(subsystem: "Test", category: .pointsOfInterest)

extension OSLog {
    func interval<T: Sendable>(name: StaticString, block: () async throws -> T) async rethrows -> T {
        let id = OSSignpostID(log: self)
        os_signpost(.begin, log: self, name: name, signpostID: id)
        defer { os_signpost(.end, log: self, name: name, signpostID: id) }
        return try await block()
    }
}
ki1q1bka

ki1q1bka2#

您看到的行为是由于您正在执行的异步任务的性质,当该任务绑定到Actor时。
iOS中的底层线程执行模型不允许抢占。也就是说,CPU永远不会从任务中“拿走”。当一个任务放弃CPU时,其他任务就有机会开始执行。
此代码:

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

is CPU bound -在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,允许actor执行另一个函数,直到 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示例。

相关问题