[随笔] Swift 异步:Task vs DispatchQueue
本文同步自「我的 Memos - 随笔」,你也可访问 x.singee.me 查看我的全部碎碎念
在 Swift 上,执行一个异步的函数大体上有两种办法
- Task
- DispatchQueue
背景知识:线程和队列
Swift 同时支持多线程和异步,因此
- 存在主线程和多个后台线程
- 每个线程存在若干队列(若干个全局 global 队列(每个优先级一个)、自定义队列(人为创建))
背景知识:队列优先级
在队列层面,存在优先级的概念(在 Task 中叫 priority,在 DispatchQueue 中叫 qos)
- userInteractive:最高优先级,适用于 UI 操作(例如动画等)(在 Task 中被弃用)
- userInitiated:较高优先级,适用于用户触发的操作
- default:中优先级,默认(在 Task 中被弃用)
- high, medium, low:高中低优先级(仅在 Task 中存在)
- utility:较低优先级,适用于耗时的后台任务
- background:最低优先级,适用于耗时的后台任务
- unspecified:继承于 Thread.current.qualityOfService(在 Task 中被弃用)
背景知识:main
main 同时隐含着两个概念:main thread 和 main queue,事实上无需特意区分(可以认为只有 main thread 才有 main queue,而 main thread 的不同 queue 之间并无特殊区别)
main 的主要用处在于其是直接和用户交互的,只有在 main 才能修改 UI、如果 main 繁忙用户会感觉到 UI 刷新卡顿
- 对于 Task 而言,在 main 执行使用
Task { @MainActor
- 对于 DispatchQueue 而言,在 main 执行使用
DispatchQueue.main
注意,Task 的 @MainActor 实际上并不是让闭包代码在 main 执行,而是让其他执行它的 concurrency-aware 代码逻辑在 main 上执行(但是目前存在非 concurrency-aware 的代码,因此可能标记了 @MainActor 实际上仍然是在非 main 执行的,这种时候需要手动切换到 main;不过这种情况 XCode 会有警告,所以不必过于担心)
DispatchQueue 相关
- 依赖的是
Grand Central Dispatch(GCD)
libdispatch
- Dispatch.main 是在主线程执行的,其他均不保证实际执行线程
- 执行顺序先进先出
- 队列类型分为 serial 和 concurrent 两种,serial 在执行完一个任务后才会开始执行另一个、concurrent 会同时执行多个任务(故不保证任务的结束顺序);main 是 serial 类型的、默认也是 serial 类型的
- 执行时存在 sync 和 async 两种,sync 会等待这个任务完成后返回,async 会直接返回
- 因上述特性,在 main 线程执行 DispatchQueue.main.sync 会导致死锁
- 无法直接获得执行结果
Task 相关
- Task 有 child 和 detached 两种类型,区别在于后者无法访问到调用方可见的变量
- 来自于 main 创建的 child Task 会始终在 main 执行,否则(除非标记 @MainActor)不保证执行的线程/队列
- 已提交的任务可取消、可获取(等待)结果(获取结果需要 await)
- 如果和 DispatchQueue 类比,可以认为 Task 与 DispatchQueue.async 执行上的行为一致
额外:RunLoop
- RunLoop.current 返回当前的线程循环、.main 返回主线程循环
- 通常情况下,无需特别注意 RunLoop.main 和 DispatchQueue.main 的区别,二者都是在 main 上执行逻辑
- RunLoop.main 中执行的逻辑可以被外部用户操作暂停,而 DispatchQueue.main 不会,因此在处理滚动时可能更希望使用 RunLoop.main,而其他通常场景则一般使用 DispatchQueue.main