如何使用Task.init 和 Task.detached

638 阅读4分钟
  • 学习动机

    在使用 Swift 的非结构化任务时,Task.init()Task.detached() 是常用来创建任务的两种方法。那么,如何根据具体需求选择合适的函数呢?通过阅读本文的讲解,我相信你将能够做出更好的决策。

  • 相同点

    Task.initTask.detached 都会创建独立的顶层异步任务。并且如果你想要取消任务,你应该引用这个任务,并且调用任务的取消方法Task.cancel()。两种方法创建的任务都是非结构化任务。

  • 不同点

    Task.init

    它会继承调用者的优先级和actor上下文。 举例来说,如果调用者是隔离在@MainActor上下文中,那么这个异步任务将会运行在主线程, 并且可以访问和修改actor上下文中的actor-isolated变量。

    Task.detached

    它会创建一个新的顶层任务,它不会继承调用者优先级和actor上下文。 举例来说,如果调用者是隔离在@MainActor上下文中,那么这个任务不会阻塞当前线程,它必须通过await访问当前actoractor-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上下文中,但是runLongTaskactor-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的并发功能。

如果您有疑问请留言或者私信。