swift 在@MainActor内生成的异步任务是否在后台运行?

2w3kk1z5  于 2023-03-22  发布在  Swift
关注(0)|答案(2)|浏览(189)

我假设异步任务继承了actor上下文,所以如果我的类标记为@MainActor,异步任务应该在主线程上运行?我认为我的假设可能有缺陷。
在我的代码中,第一个asyncTest()方法是在主线程上执行的,但由于某种原因,asyncMap()是在后台线程上执行的。
这是我测试过的游戏场:

import PlaygroundSupport
import Foundation

@MainActor
class AsyncTest {
    
    nonisolated init() {}
    
    func asyncTest() async {
        print(#function, "isMainThread:", Thread.isMainThread)
        
        let test: [String] = ["Hello", "World"]
        
        await test.asyncMap({ string in
            return string.uppercased()
        })
    }
}

extension Sequence {
    
    func asyncMap<T>(
        _ transform: (Element) async throws -> T
    ) async rethrows -> [T] {
        print(#function, "isMainThread:", Thread.isMainThread)
        
        var values = [T]()
        
        for element in self {
            try await values.append(transform(element))
        }
        
        return values
    }
}

let asyncTest = AsyncTest()
Task {
    await asyncTest.asyncTest()
}

PlaygroundPage.current.needsIndefiniteExecution = true

这使得:

asyncTest() isMainThread: true
asyncMap(_:) isMainThread: false
qvtsj1bj

qvtsj1bj1#

在测试这些东西时,最好使用一个真实的的应用程序(甚至可能更好地在设备上运行它)。此外,您的示例掩盖了关键细节。将代码结构减少到更有信息性的内容,消除糟粕并将整个事情从操场上下文中删除,这会产生错误的结果。
试试这个:

@MainActor
class AsyncTester {
    func asyncTest() async {
        NSLog("%@", #function)
        await callMeBack {
            NSLog("%@", "callback")
        }
    }
}

func callMeBack(_ callback: () -> Void) async {
    NSLog("%@", #function)
    callback()
}

class ViewController: UIViewController {
    let asyncTest = AsyncTester()
    override func viewDidLoad() {
        super.viewDidLoad()
        NSLog("%@", "start")
        Task {
            NSLog("%@", "Task")
            await asyncTest.asyncTest()
        }
    }
}

在这里,我们已经消除了Thread的概念,这在Swift 6中基本上是一个错误。但是通过使用NSLog进行日志记录,我们可以看到实际的线程数。从中我们可以了解到,asyncTest异步方法的内部和内部的回调都是在后台线程上执行的。
Task {}调用闭包的内部继承自上下文,因此它在主线程上执行。asyncTest()的内部在主线程上运行,因为它的类被标记为MainActor。但是当我们来到callMeBack时,它被声明为“in space”,我们在后台线程上(如我的What determines whether a Swift 5.5 Task initializer runs on the main thread?中所讨论的),因此回调也发生在后台线程上。
如果我们想确保callMeBack在主线程上运行,我们需要将其声明为@MainActor
就目前而言,你不能谈论独立于callMeBack的回调闭包本身的actor,因为它不是异步的。如果是,你可以将其设置为@MainActor,如下面的变体:

@MainActor
class AsyncTester {
    func asyncTest() async {
        NSLog("%@", #function)
        await callMeBack { @MainActor in
            NSLog("%@", "callback")
        }
    }
}

func callMeBack(_ callback: () async -> Void) async {
    NSLog("%@", #function)
    await callback()
}

class ViewController: UIViewController {
    let asyncTest = AsyncTester()
    override func viewDidLoad() {
        super.viewDidLoad()
        NSLog("%@", "start")
        Task {
            NSLog("%@", "Task")
            await asyncTest.asyncTest()
        }
    }
}

在这个例子中,除了callMeBack之外的所有内容都在主线程上运行,您可以通过将callMeBack标记为@MainActor来解决这个问题。

q3qa4bjr

q3qa4bjr2#

不,async方法不“继承”其调用者的actor。(Sequence,缺少@MainActor限定符)和函数本身的(它也没有MainActor限定符)。没有理由asyncMap会在主actor上运行。它不是与主actor隔离的。
因此:

  • asyncTest是在一个与主参与者隔离的类型中定义的,因此asyncTest也与主参与者隔离;
  • asyncMap是在一个类型(或扩展)中定义的,该类型不与任何特定的参与者隔离,因此这个async方法不与主参与者隔离;
  • 提供给asyncMap的闭包是在asyncTest内部创建的,asyncTest与主参与者隔离,因此这个闭包也与主参与者隔离。

值得一提的是,看看你的asyncMap想法,值得注意的是Apple已经通过AsyncSequence提供了map的异步呈现。为了利用这一点,我们可以使用Swift Async Algorithms包中的async方法从数组中创建AsyncSequence。一旦你将这个包添加到你的项目中,你可以做如下事情:

let test = ["Hello", "World"]
let sequence = test.async.map { await foo(with: $0) }

// and then

for await element in sequence {
    print(element)
}

// or

let result = await Array(sequence)

不用说,我已经将uppercased替换为对某个异步方法的调用(在我的示例中称为foo)。如果我正在做一些同步的事情,比如uppercased,我将使用标准的map并完成它:

let result = test.map { $0.uppercased() }

相关问题