Swift Concurrency 中的 Threads 与 Tasks

Swift Concurrency 中的 Threads 与 Tasks

Swift Concurrency 的引入彻底改变了我们编写异步代码的方式。它用更抽象、更安全的任务(Task)模型替代了传统的直接线程管理,旨在提高性能、减少错误并简化代码。理解线程(Threads)和任务(Tasks)之间的区别,是掌握现代 Swift 并发编程的关键。

1. 线程(Threads):系统级资源

线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。

1.1 线程的特点

  • 系统资源:线程由操作系统内核管理和调度,创建、销毁和上下文切换开销较大。

  • 并发执行 :多线程允许程序中的多个操作并发(Concurrently) 执行, potentially improving performance on multi-core systems。

  • 传统痛点

  • 高内存开销:每个线程都需要分配独立的栈空间等内存资源。

  • 上下文切换成本:当线程数量超过 CPU 核心数时,操作系统需要频繁切换线程,消耗大量 CPU 资源。

  • 优先级反转(Priority Inversion):低优先级任务可能阻塞高优先级任务的执行。

  • 线程爆炸(Thread Explosion):过度创建线程会导致系统资源耗尽、性能急剧下降甚至崩溃。

在 Grand Central Dispatch (GCD) 时代,开发者需要显式地将任务分发到主队列或全局后台队列,并时刻警惕这些线程管理问题。

2. 任务(Tasks):更高层次的抽象

Swift Concurrency 引入了 任务(Task) 作为执行异步工作的基本单位。一个任务代表一段可以异步执行的代码。

2.1 任务的特点

  • 异步工作单元:一个 Task 封装了一段异步操作的逻辑。

  • 不绑定特定线程 :Task 被提交到 Swift 的协作式线程池(Cooperative Thread Pool) 中执行,由运行时系统动态地分配到任何可用的线程上,而不是绑定到某个特定线程。

  • 结构化并发:Task 提供了结构化的生命周期管理,包括取消、优先级和错误传播。子任务会继承父任务的优先级和上下文,并确保在其父任务完成之前完成。

  • 挂起与恢复 :Task 可以在 await 关键字标记的挂起点(Suspension Point) 挂起,释放当前线程以供其他任务使用,并在异步操作完成后在某个线程上恢复执行(很可能不是原来的线程)。

2.2 任务的创建方式

Swift Concurrency 提供了几种创建任务的方式:

  1. Task 初始化器:最常用的方式,用于在非异步上下文中启动一个新的异步任务。
swift 复制代码
Task {

// 这里是异步上下文

let result = await someAsyncFunction()

print(result)

}
  1. async let 绑定:允许同时启动多个异步操作,并稍后等待它们的结果。
swift 复制代码
func fetchMultipleData() async {

async let data1 = fetchData(from: url1)

async let data2 = fetchData(from: url2)

// 两个请求同时进行

let results = await (data1, data2) // 等待两者完成

}
  1. 任务组(Task Group):用于动态创建一组并发的子任务,并等待所有子任务完成。
swift 复制代码
func processImages(from urls: [URL]) async throws -> [Image] {

try await withThrowingTaskGroup(of: Image.self) { group in

for url in urls {

group.addTask { try await downloadAndProcessImage(from: url) }

}

// 收集所有子任务的结果

return await group.reduce(into: []) { $0.append($1) }

}

}

3. Swift 的协作式线程池(Cooperative Thread Pool)

Swift Concurrency 的高效核心在于其协作式线程池

3.1 工作原理

  • 线程数量固定:线程池创建的线程数量通常与当前设备的 CPU 物理核心数相同(例如,iPhone 16 Pro 是 6 核,则线程池大小约为 6)。这避免了过度创建线程。

  • 协作而非抢占 :线程池中的线程不会像传统线程那样被操作系统强制抢占式调度。相反,任务需要主动协作(Cooperate) ,在适当的时机(即 await 挂起点)主动挂起,释放线程给其他任务使用。

  • 高效调度 :运行时系统负责将大量的 Task 高效地调度到数量有限的线程上执行。当一个任务在 await 处挂起时,线程不会空等,而是立刻去执行其他已经就绪的任务。

3.2 挂起与恢复(Suspension and Resumption)

这是理解 Swift Concurrency 非阻塞特性的关键。

swift 复制代码
struct ThreadingDemonstrator {

private func firstTask() async throws {

print("Task 1 started on thread: \(Thread.current)")

try await Task.sleep(for: .seconds(2)) // 🛑 挂起点

print("Task 1 resumed on thread: \(Thread.current)")

}

  


private func secondTask() async {

print("Task 2 started on thread: \(Thread.current)")

}

  


func demonstrate() {

Task {

try await firstTask()

}

Task {

await secondTask()

}

}

}

可能的输出

arduino 复制代码
Task 1 started on thread: <NSThread: 0x600001752200>{number = 3, name = (null)}

Task 2 started on thread: <NSThread: 0x6000017b03c0>{number = 8, name = (null)}

Task 1 resumed on thread: <NSThread: 0x60000176ecc0>{number = 7, name = (null)}

解读输出

  1. Task 1 开始在线程 3 上执行。

  2. 遇到 await Task.sleep时,Task 1 被挂起线程 3 被释放

  3. 运行时系统调度 Task 2 开始执行,它可能被分配到空闲的线程 8 上。

  4. 2 秒后,Task 1 的睡眠结束 ,变为就绪状态。运行时系统安排它恢复执行 ,但可能分配到了另一个空闲的线程 7 上。

这个过程完美展示了 Task 与 Thread 的"多对一"关系以及挂起/恢复机制如何实现线程的高效复用。

4. 与 Grand Central Dispatch (GCD) 的对比

虽然 GCD 非常强大且成熟,但 Swift Concurrency 在其基础上提供了更现代的抽象。

| 方面 | Grand Central Dispatch (GCD) | Swift Concurrency |

| :------------------ | :------------------------------------------------------------- | :-------------------------------------------------------------- |

| 抽象核心 | 队列(DispatchQueue) | 任务(Task) |

| 线程模型 | 动态创建线程,数量可能远超过 CPU 核心数,可能导致线程爆炸 。 | 协作式线程池,线程数 ≈ CPU 核心数,从根本上避免线程爆炸。 |

| 阻塞与挂起 | 提交到队列的 Block 会阻塞底层线程 (如果内部执行同步操作)。 | 在 await挂起任务释放底层线程,不会阻塞。 |

| 性能 | 优秀,但线程过多时上下文切换开销大。 | 更优,极少的线程处理大量任务,减少上下文切换,CPU 更高效。 |

| 语法与可读性 | 基于闭包的回调,嵌套地狱(Callback Hell)风险。 | 线性化的 async/await 语法,代码更清晰、更易读。 |

| 状态管理 | 需要手动处理引用循环([weak self])。 | 结构化并发减少了循环引用风险。 |

| 安全性 | 需要开发者自己避免数据竞争(Data Race)。 | 通过 ActorSendable 协议在编译时提供数据竞争安全。 |

4.1 性能对比:线程更少,性能更好?

这听起来有悖常理,但却是事实。GCD 的线程爆炸问题会导致内存压力增大和大量的上下文切换,反而消耗了 CPU 资源,使得真正用于执行任务的 CPU 周期减少。

Swift Concurrency 的协作式模型通过以下方式提升效率:

  • Continuations :挂起任务时,其状态(局部变量、执行位置等)被保存为一个 Continuation 对象。线程本身被释放,可以立即去执行其他任务。这比传统的线程阻塞和唤醒要轻量得多。

  • 始终前进:线程池中的线程几乎总是在执行有效工作,而不是空转或忙于切换。这使得单位时间内可以完成更多工作。

5. 常见误区与澄清

在从 GCD 转向 Swift Concurrency 时,需要扭转一些"线程思维"。

| 误区 | 正解 |

| :---------------------------------------- | :------------------------------------------------------------------------------------------------ |

| 每个 Task 都会创建一个新线程 | Task 与线程是多对一的关系。大量 Task 共享一个小的线程池。 |

| await 会阻塞当前线程 | await挂起当前 Task ,并释放当前线程 供其他 Task 使用。这是非阻塞的。 |

| Task 会按创建顺序执行 | Task 的执行顺序没有保证,取决于运行时系统的调度策略、优先级和挂起点。 |

| 必须在主线程上更新 UI | ✅ 正确。但在 Swift Concurrency 中,更推荐使用 @MainActor 来隔离 UI 相关代码,而不是手动派发到主队列。 |

6. 从"线程思维"到"任务思维"

开发者需要实现一个思维转变:

| 线程思维 (GCD Mindset) | 任务思维 (Task Mindset) |

| :----------------------------------------- | :------------------------------------------------------ |

| "这段重计算要放到后台线程。" | "这段计算是个异步任务,系统会帮我调度。" |

| "完成后需要手动派发回主线程更新 UI。" | "用 @MainActor 标记这个函数,确保它在主线程运行。" |

| "创建太多并发队列会不会导致线程爆炸?" | "线程数量由系统自动管理,我只需专注业务逻辑和创建合理的 Task。" |

7. 实践中的差异:Thread.sleep 与 Task.sleep

这个例子能深刻体现阻塞与挂起的区别。

  • Thread.sleep(forTimeInterval:) :这是一个阻塞 式调用。它会使当前所在的线程停止工作指定的时间。如果这个线程是协作线程池中的一员,它就相当于被"卡住了",无法为其他任务服务,减少了有效工作线程数。

  • Task.sleep(for:) :这是一个非阻塞 式挂起。它会使当前 Task 挂起指定的时间,但当前任务所占用的线程会立刻被释放,并返回线程池中为其他就绪的 Task 服务。时间到后,Task 会被重新调度到某个可用线程上恢复执行。

结论 :在 Swift Concurrency 中,绝对不要使用 Thread.sleep ,它会破坏协作模型。始终使用 Task.sleep

8. 如何选择:Swift Concurrency 还是 GCD?

尽管 Swift Concurrency 更现代,但 GCD 仍有其价值。

  • 使用 Swift Concurrency (Task) 当:

  • 项目基于 Swift 5.5+。

  • 想要更安全、更易读的异步代码(async/await)。

  • 希望获得更好的性能并避免线程问题。

  • 需要利用 Actor 等数据竞争安全特性。

  • 使用 Grand Central Dispatch (GCD) 当:

  • 维护旧的、大规模使用 GCD 的代码库,迁移成本高。

  • 需要进行非常底层的线程控制(虽然绝大多数场景不需要)。

  • 与某些高度依赖 GCD 的 C API 或旧框架交互。

混合使用 :在实际项目中,两者可以共存。你可以在 Swift Concurrency 的 Task 内部使用 DispatchQueue 进行特定的操作,但要注意避免不必要的线程跳跃和性能损耗。

9. 深入底层:任务、作业与执行器(Tasks, Jobs, Executors)

为了更深入地理解,可以了解一些运行时概念:

  • 作业 (Job) :任务是比 Task 更小的执行单位。一个 Task 在编译时会被分解成多个连续的 Job 。每个 Job 是一个同步执行的代码块,位于两个 await 挂起点之间。Job 是运行时系统实际调度的单位。

  • 执行器 (Executor) :是一个服务,负责接收被调度的 Job 并安排线程来执行它。系统提供了全局的并发执行器(负责一般任务)和主执行器(负责 @MainActor 任务)。开发者通常不需要直接与之交互。

总结

Swift Concurrency 中的 ThreadsTasks 是不同层次的概念:

  • Thread系统级的底层资源,由操作系统管理,创建和切换开销大。Swift Concurrency 建立在线程之上,但开发者不再需要直接与之交互。

  • Task语言级的高层抽象,代表一个异步工作单元。它帮助开发者摆脱繁琐且易错的线程管理,专注于业务逻辑。

Swift Concurrency 的核心优势在于其协作式线程池 模型和挂起/恢复机制。它通过以下方式实现高效并发:

  1. 限制线程数量(与 CPU 核心数一致),避免线程爆炸。

  2. 使用 await 作为挂起点,任务在此主动释放线程,实现非阻塞。

  3. 利用 Continuations 保存挂起状态,实现任务在不同线程上的恢复。

  4. 通过 Actor 和结构化并发提供编译期的数据竞争安全

最终,开发者应从"线程思维"转向"任务思维",信任运行时系统会做出最优的线程调度决策,从而编写出更清晰、更安全、更高效的高并发代码。

原文:xuanhu.info/projects/it...

相关推荐
2501_916013744 小时前
iOS 26 系统电耗分析实战指南 如何检测电池掉电、液体玻璃导致的能耗变化
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915921434 小时前
iOS 原生开发全流程解析,iOS 应用开发步骤、Xcode 开发环境配置、ipa 文件打包上传与 App Store 上架实战经验
android·macos·ios·小程序·uni-app·iphone·xcode
低调小一4 小时前
双端 FPS 全景解析:Android 与 iOS 的渲染机制、监控与优化
android·ios·kotlin·swift·fps
一支鱼5 小时前
从一个前端程序员的角度来看iPhone 17 与 iOS 26 的 Web 性能与交互革新
前端·ios·产品
00后程序员张5 小时前
iOS 26 帧率测试实战指南,Liquid Glass 动画性能、滚动滑动帧率对比、旧机型流畅性与 uni-app 优化策略
android·ios·小程序·uni-app·cocoa·iphone·webview
游戏开发爱好者86 小时前
iPhone HTTPS 抓包实战,原理、常见工具、SSL Pinning 问题与替代工具的解决方案
android·ios·小程序·https·uni-app·iphone·ssl
Digitally6 小时前
如何将联系人从iPhone转移到iPhone的7种方法
ios·iphone
开开心心loky7 小时前
[iOS] YYModel 初步学习
学习·ios·objective-c·cocoa
游戏开发爱好者89 小时前
App 上架平台全解析,iOS 应用发布流程、苹果 App Store 审核步骤
android·ios·小程序·https·uni-app·iphone·webview