[随笔] 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