3337 字
17 分钟
Swift 多线程
Swift Structured Concurrency
所谓的 结构化并发,就是 Task 之间会形成一个树形结构,父 Task 会等待所有子 Task 完成后再结束。
async/await 基础
import Foundation
func fetchData() async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "数据加载完成"
}
async let data = fetchData() // 异步 let 赋值
let data = await data // 等待结果
async let 只能出现在异步上下文中 (Task closure、async function 以及 async closure)
注意
会自动、隐式地取消未完成的 async let 任务。并且如果 async let 任务抛出错误,父 Task 会自动取消。
func makeDinner() async throws -> Meal {
async let rng = fetchRandomNumber()
async let cmp = fetchComputation()
async let res = fetchResult()
// implicitly: cancel rng
// implicitly: cancel cmp
// implicitly: cancel res
// implicitly: await rng
// implicitly: await cmp
// implicitly: await res
}
Task
- 子 Task 的生命周期不会超出父 Task 的范围
- 未处理的 error 会自动从子 Task 传播到父 Task
- 子 Task 默认会继承父 Task 的优先级
- 父子 Task 间会共享 Task-local data
- 父 Task 取消时,子 Task 会自动取消
import Foundation
let handle = Task.detached(priority: .background) {
await Task.sleep(nanoseconds: 500_000_000)
return "Detached Task 完成"
}
Task {
let result = await handle.value
print(result)
}
TaskGroup
- 只能通过
withTaskGroup、withThrowingTaskGroup
创建 Task group 实例 - 同一 group 中所有子任务的结果类型必须相同
- 子任务的生命周期不会超出 group 生命周期,因此当
group(withTaskGroup,withThrowingTaskGroup)
方法返回时就意味着所有子任务都已完成或 cancel - 通过
for await ... in
可以遍历所有子任务的运行结果。顺序是子任务完成的顺序,而非子任务添加的顺序 - 内部抛出错误时 (如某个子任务抛出异常),所有未完成的子任务都将被 cancel
- group 还是会隐式的等待所有子任务完成才返回。即使不等待 group 的结果,group 也会在所有子任务完成后才返回
import Foundation
func performMultiple() async {
await withTaskGroup(of: Int.self) { group in
for i in 1...3 {
group.addTask {
await Task.sleep(nanoseconds: UInt64(i) * 300_000_000)
return i * i
}
}
for await square in group {
print("计算结果:\(square)")
}
}
}
Task {
await performMultiple()
}
Actor
import Foundation
actor Counter {
private var value = 0
func increment() { value += 1 }
func get() -> Int { value }
}
let counter = Counter()
Task.detached {
await counter.increment()
let v = await counter.get()
print("actor 计数:\(v)")
}
全局 Actor 与主线程隔离
- 本质上是一个 marker type,其同步功能是借助 shared 属性提供的 actor 实例完成的
- 可用于修饰类型定义 (如:class、struct、enum,但不能用于 actor)、方法、属性、Closure等
@globalActor
public struct MyGlobalActor {
public actor MyActor { }
public static let shared = MyActor()
}
@MyGlobalActor var globalVar: Int = 1
所有标记 @MyGlobalActor
的对象之间形成一个以 MyGlobalActor 为界的 actor-isolated。
特例:
- 被
MainActor
修饰的方法、属性等都将在主线程上执行。
坑
- actor 是通过 mailbox 机制串行执行外部调用来保障数据安全,actor 方法内部存在 Data races,actor 无法解决。 因此,尽量避免在其中手动开启子线程、使用GCD等,否则需要使用传统手法 (如 lock) 解决因此引起的多线程问题。
- 被传递出来的实例得不到 actor 的保护,需要实现
@Sendable
协议,确保传递的实例是线程安全的。
Sendable
值语义 (Value semantics) 类型在传递时 (如作为函数参数、返回值等) 是会执行拷贝操作的,也就是它们跨 Actor 传递是安全的。
- 值类型(Int、String、Bool、无引用成员 struct/enum)天然 Sendable。
- Actor 类型自动遵守 Sendable。
- class 需主动声明 Sendable,且必须 final、属性 immutable。
class Player {
let score: Int
let name: String
}
// 遵守 Sendable 协议
final class Player: Sendable {
let score: Int
let name: String
}
// 使用 struct
struct Player: Sendable {
let score: Int
let name: String
}
// 标注 unchecked
class Player: @unchecked Sendable {
let score: Int
let name: String
}
对比 async-let TaskGroup
- 能用 async let 就不用 Task group
- async let 创建子任务的数量是静态的,而 Task group 可以动态创建子任务
- async let 等待子任务完成的顺序是固定,无法做到按子任务完成顺序取结果
使用 TaskGroup 可以很轻松实现以最快的服务器优先返回结果的功能:
func fastestResponse() async -> Int {
await withTaskGroup(of: Int.self) { group in
group.addTask { await requestFromServer1() }
group.addTask { await requestFromServer2() }
return await group.next()!
}
}
关系
在 Swift Concurrency 中,async/await 并不是单纯的语法糖,而是依赖于底层的 任务(Task) 和 执行器(Executor) 来调度和执行异步工作。具体来说:
- 编译期:状态机+续延(Continuation)
- 每个标记为 async 的函数,编译器会把它拆解成一个状态机(state machine),并在遇到 await 时生成一个续延(continuation)闭包,负责在异步操作完成后恢复执行。
- 这一部分完全在编译阶段完成,和 runtime 的具体实现无关。
- 运行时:任务(Task)调度
- 当你调用一个 async 函数时,实际上是在某个 Task 上运行这段状态机逻辑。最外层,如果你在局上下文直接调用 await,编译器会自动为你隐式创建一个托管在 主执行器(MainActorexecutor) 或全局执行器上的 “主任务” (main task)。
- 你也可以显式地使用 Task { … } 或 Task.detached { … } 来创建新的 Task,把异步闭包给后台执行。
- 底层的 Task 类型是对 Swift Concurrency runtime(C++/C 实现)的高级封装,负责:
- 管理任务的生命周期(创建、挂起、恢复、取消)
- 在不同执行器(如主线程、全局并发队列、actor 专属队列)之间切换
- 实现超时、优先级等机制
- 挂起与恢复:
- await 会挂起当前 Task,把状态机和上下文打包成一个续延,交给底层 runtime 存储,并立即回控 制权。
- 当被等待的异步操作完成(如网络请求回包、I/O 事件到达),runtime 会把对应的续延重新排入Task 调度队列,在适当的执行器上恢复执行。
- Swift 的异步逻辑最终是运行在 Task 之上的,没有 Task,就没有运行时调度和挂起/恢复能力。
func fetchData() async -> Data {
let url = URL(string: "https://example.com")!
let (data, _) = try! await URLSession.shared.data(from: url)
return data
}
// 隐式地在当前 Task(如主线程 Task)中调用:
Task {
let d = await fetchData()
print("Got \(d.count) bytes")
}
// 显式创建一个后台 Task:
Task.detached(priority: .background) {
let d = await fetchData()
// 切回主线程更新 UI
await MainActor.run {
imageView.image = UIImage(data: d)
}
}
- 上面两种调用方式都依赖 Task 来承载执行和调度。
- await fetchData() 会在内部把当前 Task 挂起,等到 URLSession 完成后再恢复。
原理
在 Swift 中,async/await 并不是简单地“语法糖包裹”在 Task 之上,而是整个编译器+运行时协同工作的结果:
- 编译器层面(SIL 转换为状态机)
- 当你写一个 async func foo() 时,编译器在 SIL(Swift Intermediate Language阶段会把这个函数拆成一系列“状态”与“待恢复点”(suspension points)。
- 每遇到一个 await,编译器会在对应位置插入对底层 continuation(续体)的创建和挂起用(swift_continuation_create),并生成在未来某个时刻恢复执行的“resume”代码。
- 最终,整个 async 函数就像一个带有状态机的协程,内部维护一个保存了局部变量和下一个行状态的上下文。
- 运行时层面(libswiftConcurrency 调度)
- Swift 的并发运行时(libswiftConcurrency)负责管理这些状态机的实际执行。它提了:
- 任务(Task)对象:每个异步调用都运行在某个 Task 中,Task 本质上是一个并发单元,含了调度信息、优先级、执行器(Executor)等。
- 执行器(Executor)和线程池:默认使用系统线程池,也支持 MainActor、全局并发队列不同执行环境。Task 会被放入相应执行器排队,运行时负责公平调度、优先级继承等。
- 续体(Continuation)机制:当某个 await 遇到需要挂起的操作(例如网络请求、计时器另一个 async 函数的结果)时,运行时会将当前 Task 的状态打包到一个 Continuation,交异步操作,等操作完成后再把这个 Continuation 投递回执行器以恢复执行。
- async/await 与 Task 关系
- 不是同一回事:async/await 是编译器层面的协程语法、通过状态机和续体实现挂起/恢复Task 则是运行时层面的执行单元,负责调度和上下文管理。
- 又密切关联:所有异步函数的调用都必须在某个 Task 上运行——你在顶层可以通过 Task {await foo() } 显式开启,也可以在已有的异步上下文(如从另一个 async 函数)自动复用前 Task。
- 换句话说,async 函数的状态机挂起时,会把当前运行的 Task 交给运行时管理;恢复时,又会在同一个或指定的执行器里继续以该 Task 的身份执行。
简单归纳:
- 编译器 把 async/await 转成带续体的状态机;
- 运行时 用 Task、执行器和续体机制来调度这些状态机的挂起与恢复;
- Task 是承载异步状态机的执行容器,而 async/await 则是操作这个容器内部状态的语言特性。
Grand Central Dispatch(GCD)
- DispatchQueue:系统级队列,分串行(serial)和并发(concurrent)。
- 主队列(DispatchQueue.main):主线程,适合 UI 更新。
- 全局队列(DispatchQueue.global()):系统按 QoS 分配线程。
import Foundation
// 串行队列
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async {
print("串行任务 1 - 线程:\(Thread.current)")
}
serialQueue.async {
print("串行任务 2 - 线程:\(Thread.current)")
}
// 并发队列
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
for i in 1...3 {
concurrentQueue.async {
print("并发任务 \(i) - 线程:\(Thread.current)")
}
}
// 主队列示例(UI 更新)
DispatchQueue.global().async {
let result = (0..<1_000_000).reduce(0, +)
DispatchQueue.main.async {
print("结果:\(result),在主线程更新 UI")
}
}
QoS(Quality of Service)
用来标识一段任务(work item)的重要性和紧急程度,系统会据此给它分配合适的线程优先级、CPU 时间片和其它资源
- .userInteractive 最高优先级,用于需要立刻更新 UI 或者与用户互动的操作(动画、手势响应等)。
- .userInitiated 次高优先级,用在用户主动发起、并希望尽快得到结果的任务(比如从网络加载数据后立即显示)。
- .default 默认优先级,等同于未显式指定 QoS 时的级别。
- .utility 中等偏低优先级,适合需要一定时间才能完成,但用户可容忍等待的任务(如下载、导入、导出、数据库查询);系统也会考虑节能。
- .background 最低优先级,用于不影响用户体验的长期后台任务(如数据备份、预取、清理缓存等),系统会最大程度地节省资源。
- .unspecified 未指定,由系统根据上下文决定最合适的调度策略。
OperationQueue
- Operation(子类/BlockOperation)比 GCD 更高级,支持依赖、取消、优先级。
- OperationQueue 管理 Operation,可控制并发数。
import Foundation
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
class MyOperation: Operation {
override func main() {
if isCancelled { return }
print("自定义 Operation 执行 - 线程:\(Thread.current)")
}
}
let op1 = MyOperation()
let op2 = BlockOperation {
print("BlockOperation 执行 - 线程:\(Thread.current)")
}
op2.addDependency(op1) // op2 依赖 op1
queue.addOperations([op1, op2], waitUntilFinished: false)
NSThread
- 最基础的线程封装,直接操控 Thread 对象。
- 仅极少数需手动管理线程生命周期场景使用。
import Foundation
func threadTask() {
print("NSThread 任务 - 线程:\(Thread.current)")
}
let thread = Thread(target: self, selector: #selector(threadTask), object: nil)
thread.name = "com.example.thread"
thread.start()
建议
选型
- 简单并发:优先 GCD(DispatchQueue)。
- 依赖管理:OperationQueue + Operation。
- 结构化、生命周期明确:优先 Swift Concurrency(async/await + Task + Actor)。
- 避免竞争:用串行队列、Actor 或锁(DispatchSemaphore/NSLock)。
- UI 更新:务必在主线程(DispatchQueue.main 或 MainActor)执行。
陷阱
- 作用域:async let 和 TaskGroup 创建的子任务,其生命周期严格受限于当前作用域,不能将未完成的 async let/TaskGroup 结果逃逸到外部,否则会导致未定义行为或编译错误。
- 内存管理:async let 绑定的变量在作用域结束时会自动 await 并释放资源,避免内存泄漏。
- 避免逃逸:不要将 async let/TaskGroup 的结果或未完成的 Task 传递到作用域外部。
- Data Race:即使使用 Actor,也要注意内部并发访问(如 actor 内部手动开线程)。
- Swift 的 actor 保证它的隔离状态在并发调用之间不会被同时访问,但如果你在 actor 内部手动启动线程或 Task,并在这些并发执行的任务里直接读写 actor 的内部属性,就绕过了 actor 的隔离保护,依然会发生数据竞争。
- 死锁:避免在主线程等待异步结果,或在 actor 内部 await 自己的方法。
- 在主线程(main thread)同步地等待异步结果(比如使用 .result、.wait() 或者在 UI 回调里直接调用 Task { … }.value)会阻塞主线程的运行循环,从而无法调度异步任务,导致死锁。
- 在同一个 actor 的方法里 await 自己的另一方法时,会因为 actor 的串行执行模型(同一时间只处理一个任务)而互相等待,形成死锁。
Xcode 提供 Thread Sanitizer(可在 Scheme 设置中开启),能检测数据竞争和线程安全问题。
参考链接
总结:Swift 并发体系已高度现代化,推荐优先采用结构化并发和 Actor,GCD/OperationQueue 作为补充。