3337 字
17 分钟
Swift 多线程
2025-05-20

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) 来调度和执行异步工作。具体来说:

  1. 编译期:状态机+续延(Continuation)
    • 每个标记为 async 的函数,编译器会把它拆解成一个状态机(state machine),并在遇到 await 时生成一个续延(continuation)闭包,负责在异步操作完成后恢复执行。
    • 这一部分完全在编译阶段完成,和 runtime 的具体实现无关。
  2. 运行时:任务(Task)调度
    • 当你调用一个 async 函数时,实际上是在某个 Task 上运行这段状态机逻辑。最外层,如果你在局上下文直接调用 await,编译器会自动为你隐式创建一个托管在 主执行器(MainActorexecutor) 或全局执行器上的 “主任务” (main task)。
    • 你也可以显式地使用 Task { … } 或 Task.detached { … } 来创建新的 Task,把异步闭包给后台执行。
    • 底层的 Task 类型是对 Swift Concurrency runtime(C++/C 实现)的高级封装,负责:
      1. 管理任务的生命周期(创建、挂起、恢复、取消)
      2. 在不同执行器(如主线程、全局并发队列、actor 专属队列)之间切换
      3. 实现超时、优先级等机制
  3. 挂起与恢复:
    • await 会挂起当前 Task,把状态机和上下文打包成一个续延,交给底层 runtime 存储,并立即回控 制权。
    • 当被等待的异步操作完成(如网络请求回包、I/O 事件到达),runtime 会把对应的续延重新排入Task 调度队列,在适当的执行器上恢复执行。
  4. 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 之上,而是整个编译器+运行时协同工作的结果:

  1. 编译器层面(SIL 转换为状态机)
    • 当你写一个 async func foo() 时,编译器在 SIL(Swift Intermediate Language阶段会把这个函数拆成一系列“状态”与“待恢复点”(suspension points)。
    • 每遇到一个 await,编译器会在对应位置插入对底层 continuation(续体)的创建和挂起用(swift_continuation_create),并生成在未来某个时刻恢复执行的“resume”代码。
    • 最终,整个 async 函数就像一个带有状态机的协程,内部维护一个保存了局部变量和下一个行状态的上下文。
  2. 运行时层面(libswiftConcurrency 调度)
    • Swift 的并发运行时(libswiftConcurrency)负责管理这些状态机的实际执行。它提了:
    • 任务(Task)对象:每个异步调用都运行在某个 Task 中,Task 本质上是一个并发单元,含了调度信息、优先级、执行器(Executor)等。
    • 执行器(Executor)和线程池:默认使用系统线程池,也支持 MainActor、全局并发队列不同执行环境。Task 会被放入相应执行器排队,运行时负责公平调度、优先级继承等。
    • 续体(Continuation)机制:当某个 await 遇到需要挂起的操作(例如网络请求、计时器另一个 async 函数的结果)时,运行时会将当前 Task 的状态打包到一个 Continuation,交异步操作,等操作完成后再把这个 Continuation 投递回执行器以恢复执行。
  3. 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 作为补充。

Swift 多线程
https://blog.lpkt.cn/posts/swift-multi-thread/
作者
lollipopkit
发布于
2025-05-20
许可协议
CC BY-NC-SA 4.0