-
学习动机
在使用 Swift 的非结构化任务时,
Task.init()和Task.detached()是常用来创建任务的两种方法。那么,如何根据具体需求选择合适的函数呢?通过阅读本文的讲解,我相信你将能够做出更好的决策。 -
相同点
Task.init和Task.detached都会创建独立的顶层异步任务。并且如果你想要取消任务,你应该引用这个任务,并且调用任务的取消方法Task.cancel()。两种方法创建的任务都是非结构化任务。 -
不同点
Task.init它会继承调用者的优先级和
actor上下文。 举例来说,如果调用者是隔离在@MainActor上下文中,那么这个异步任务将会运行在主线程, 并且可以访问和修改actor上下文中的actor-isolated变量。Task.detached它会创建一个新的顶层任务,它不会继承调用者优先级和
actor上下文。 举例来说,如果调用者是隔离在@MainActor上下文中,那么这个任务不会阻塞当前线程,它必须通过await访问当前actor中actor-isolated变量。 -
实践
下面我们通过几个具体使用案例加深理解。
例 1
actor Test { func doSomething() { var i = 0 //这里不能不能修改 变量`i`的原因是, 变量 `i` 是局部变量,没有被隔离在当前`actor` 上下文,此时存在数据竞争。 for _ in 0..<6 { Task.init { // error: Mutation of captured var 'i' in concurrently-executing code i += 1 } } } }这里不能修改变量
i的原因是, 变量i是局部变量,没有被隔离在当前actor上下文,此时存在数据竞争。例 2
actor Test { var i = 0 func doSomething() { // 这里能修改i 是因为,i是actor-isolated,同一时间只能有一个任务能访问 i for _ in 0..<6 { Task.init { // self是Sendable类型 self.i += 1 } } } }解释:这里能修改变量i是因为 变量i默认隔离在actor上下文中,而Task.init运行在当前actor上下文,可以访问 和修改actor-isolated变量。例 3
@MainActor class Test { func doSomething() async{ var i = 0 // 这里能修改i 是因为,Task.init 只能运行在主线程上,修改i是安全的 for _ in 0..<6 { Task.init { i += 1 } } } }解释: 这里能修改变量i是因为,@MainActor 使得dosomething()运行在主线程中,Task.init 继承了actor上下文,主线程访问变量i是同步的,没有数据竞争。 而例1则是运行在多个线程中,存在数据竞争。
例 4
actor Test {
var i = 0
func doSomething() {
for _ in 0..<6 {
Task.detached {
// error: Actor-isolated property 'i' can not be mutated from a Sendable closure
self.i += 1
}
}
}
}
解释: 变量i默认隔离,但是Task.detached不运行当前actor上下文中
解决:如果想要修改变量i, 只能通过await访问当前actor的方法,比如在actor中增加一个修改方法。
actor Test {
var i = 0
func doSomething() {
for _ in 0..<6 {
Task.detached {
await self.increment()
}
}
}
func increment() {
i += 1
}
}
例 5
// NSViewController 使用了@MainActor标注
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 阻塞主线程
Task.detached {
await self.runLongTask()
}
}
//运行在主线程中
func runLongTask() async -> Int {
var res = 0
for i in 0..<99999999 {
res += i
}
return res
}
}
解释: 首先我们知道ViewController使用了@MainActor标注,这会使ViewController隔离在 main actor中,运行在主线程。尽管Task.detached创建的任务不会运行在当前的actor上下文中,但是runLongTask是actor-isolated状态,所有它会运行在主线程中,阻塞UI.
解决方案: 可以将这个耗时任务标注为非隔离nonisolated,这样就不会阻塞UI.
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task.detached {
await self.runLongTask()
}
}
//可以将这个耗时任务标注为非隔离`nonisolated`
nonisolated func runLongTask() async -> Int {
var res = 0
for i in 0..<99999999 {
res += i
}
return res
}
}
-
结语
通过本文的讲解,我们深入探讨了Task.init()和Task.detached()的使用场景和区别。了解它们在不同上下文中的行为特性,对于编写安全、高效的异步代码至关重要。在实际开发中,选择合适的任务创建方式,能够有效避免数据竞争和阻塞问题,提高代码的可维护性和性能。
希望通过这些实例和解释,您能够更清晰地理解如何根据具体需求做出最佳选择,从而在实际应用中更好地利用Swift的并发功能。
如果您有疑问请留言或者私信。