是什么决定了 Swift 5.5 任务初始值设定项是否在主线程上运行?

mat*_*att 24 async-await swift swift5

这是我之前的asyncDetached 在 MainActor call 之后回到主线程的后续。

这是 iOS 视图控制器的完整代码:

import UIKit

func test1() {
    print("test1", Thread.isMainThread) // true
    Task {
        print("test1 task", Thread.isMainThread) // false
    }
}
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        test1()
        test2()
    }

    func test2() {
        print("test2", Thread.isMainThread) // true
        Task {
            print("test2 task", Thread.isMainThread) // true
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这两个函数test1test2是相同的,并且是从同一位置调用的。然而,其中一个operation:在后台线程上运行其任务初始值设定项函数,另一个在主线程上运行。

是什么决定了这一点?我只能认为这与声明方法的位置有关。但这与声明方法的位置有什么关系呢?

mat*_*att 16

我认为规则必须是 MainActor 方法中的任务初始值设定项在主线程上运行。

视图控制器的所有方法默认都是MainActor方法;另外,我观察到,如果我声明test2nonisolated,其 Task 操作将在后台线程而不是主线程上运行。

那么,我的猜测是,这是任务初始值设定项的操作从其上下文“继承”的规则的示例:

  • test2是一个 MainActor 方法;它在主线程上运行,因此任务操作“继承”它。

  • test1没有标记任何特殊线程。test1 本身运行在主线程上,因为是在主线程上调用的;但它没有被标记为在主线程上运行。因此,它的任务操作会退回到在后台线程上运行。

无论如何,这是我的理论,但我觉得奇怪的是,相关 WWDC 视频中没有明确阐明这条规则。

而且,eventest2只是一种“弱”方式的 MainActor 方法。如果它确实是一个 MainActor 方法,那么您无法在没有await. 但您可以,正如此版本的代码所示:

func test1() {
    print("test1", Thread.isMainThread) // true
    Task {
        print("test1 task", Thread.isMainThread) // false
    }
}
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        test1()
        Task.detached {
            self.test2()
        }
    }

    func test2() {
        print("test2", Thread.isMainThread) // false
        Task {
            print("test2 task", Thread.isMainThread) // true
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我觉得这确实很奇怪,而且我很难阐明什么规则来管理这种无情的上下文切换行为,所以我不认为这件事已经解决了。

  • @Bill这显然是一个已知的权衡... https://developer.apple.com/forums/thread/693660 (2认同)
  • 值得注意的是,在 swift 5.9 中,这不再是行为,并且该答案的代码甚至无法编译。一旦你添加一个await到`self.test2()`,打印输出是`test1 true test1 task false test2 true test2 task true` (2认同)

Sou*_*unt 9

This is because of how actor isolation and task creation work in swift. Actors have serial executor that processes one task at a time to synchronize mutable state. So any method that is isolated to the actor will run on the actor's executor. And when creating Task with Task.init the newly created task inherits the actor's context (isolated to the parent actor) it was created in (unless you specify a different global actor explicitly when creating task) and then processed by the actor's executor.

What's happening here is your ViewController class and all its methods and properties are MainActor isolated since UIViewController is MainActor isolated and you are inheriting from it. So your test2 method is isolated to MainActor and when you are creating a task inside test2 the new task inherits the MainActor context and gets executed by MainActor on the main thread.

But this behaves differently from your test1 method because your test1 method isn't isolated to MainActor. When you are calling test1 from viewDidLoad the synchronous part of test1 is executed on MainActor as part of the current task but when you are creating a new task is test1, since test1 isn't isolated to MainActor, your new task isn't executed on it.

To have the same behavior in test1 as test2, you can mark your method to be isolated to MainActor by applying the @MainActor attribute to function definition:

@MainActor
func test1() {
    print("test1", Thread.isMainThread) // true
    Task {
        print("test1 task", Thread.isMainThread) // true
    }
}
Run Code Online (Sandbox Code Playgroud)