深入理解 Swift 5.5+ 的现代并发模型,掌握如何编写安全高效的多线程代码
引言:为什么需要新的并发模型?
在传统 iOS/macOS 开发中,我们使用 GCD(Grand Central Dispatch)或 OperationQueue 来处理并发任务。然而,这些技术存在一些痛点:
- 回调地狱:多层嵌套的回调难以阅读和维护
- 手动内存管理:容易忘记 weak self 导致内存泄漏
- 线程爆炸:过度创建线程消耗系统资源
- 数据竞争:共享状态需要手动加锁,容易出错
Swift 5.5 引入的 async/await 和结构化并发解决了这些问题,提供了更安全、更简洁的并发编程方式,iOS 13以上是支持的。
第一部分:async/await 基础语法
1.1 异步函数声明
swift
// 传统回调方式
func fetchUser(completion: @escaping (Result<User, Error>) -> Void)
// 异步函数方式
func fetchUser() async throws -> User
1.2 异步函数调用
swift
// 使用 await 调用异步函数
do {
let user = try await fetchUser()
print("用户: \(user.name)")
} catch {
print("错误: \(error)")
}
第二部分:async let 与结构化并发
2.1 并发启动多个任务
swift
// 同时启动多个异步任务
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser() // 立即开始
async let orders = fetchOrders() // 立即开始
async let messages = fetchMessages() // 立即开始
// 等待所有任务完成
return try await Dashboard(
user: user,
orders: orders,
messages: messages
)
}
2.2 与顺序执行的对比
swift
// 并发执行(总耗时 ≈ 最慢的任务)
async let a = taskA() // 0-1秒
async let b = taskB() // 0-2秒
let results = await (a, b) // 总耗时: 2秒
// 顺序执行(总耗时 = 所有任务时间之和)
let a = await taskA() // 0-1秒
let b = await taskB() // 1-3秒(等A完成后才开始)
// 总耗时: 3秒
2.3 重要概念澄清
Q: async let user = fetchUser() 立即返回什么? A: 它不立即返回数据,而是返回一个异步任务句柄 。实际数据在 await 时获取。
Q: 多个 async let 相当于 GCD 的异步任务吗? A: 相似但有重要区别。async let 是结构化并发的一部分,任务生命周期自动管理,支持取消和错误传播。
第三部分:数据安全与线程调度
3.1 数据竞争的解决方案
方案一:使用 Actor(银行柜台模型)
swift
actor UserCache {
private var storage: [String: User] = [:]
func getUser(id: String) -> User? {
return storage[id]
}
func setUser(_ user: User, for id: String) {
storage[id] = user
}
}
// 使用时自动序列化访问
let cache = UserCache()
let user = await cache.getUser(id: "123") // 自动排队等待
原理:编译器强制同一时间只有一个任务能访问 Actor 内部状态,通过消息传递模型确保安全。
方案二:使用值语义(发复印件模型)
swift
struct UserProfile {
let user: User
var settings: Settings
// 结构体是值类型,复制安全
}
func processProfile(profile: UserProfile) async {
// 每个任务获取独立的副本
async let task1 = {
var copy = profile
copy.settings.theme = .dark
return copy
}()
async let task2 = {
var copy = profile
copy.settings.fontSize = 16
return copy
}()
let results = await (task1, task2) // 独立修改,互不影响
}
原理:通过复制而非共享,从根本上消除数据竞争的可能性。
3.2 智能线程调度
Q: async let 任务在哪个线程执行? A: Swift 并发运行时智能决定,基于以下因素:
- 当前线程负载 - 太忙就调度到其他线程
- 任务类型 - I/O密集型 vs CPU密集型
- 优先级 - 高优先级任务可能更快执行
- 硬件资源 - CPU核心数、当前负载
- 执行器约束 - 如
@MainActor强制主线程
智能调度的具体表现:
swift
@MainActor
func updateUIWithData() async {
// 从主线程调用,但会自动优化
async let data = fetchHeavyData() // 运行时:这个会阻塞 → 调度到后台线程
let processed = await process(data) // 可能在后台线程继续处理
// 更新UI时自动回到主线程
self.label.text = processed.title
}
3.3 什么时候需要显式控制线程?
swift
// 1. UI操作必须主线程
@MainActor
func updateUI() {
// 编译时确保在主线程
}
// 2. CPU密集型长时间计算
func processImage(_ image: UIImage) async -> UIImage {
// 明确指定在独立线程执行
return await Task.detached {
return image.applyFilters() // 耗时的图像处理
}.value
}
// 3. 不应该干预的案例
// ❌ 不要这样:破坏了智能调度
Task {
DispatchQueue.global().async {
await someAsyncWork()
}
}
// ✅ 应该这样:信任运行时
Task {
await someAsyncWork() // 让系统决定最佳执行方式
}
第四部分:实际应用模式
4.1 网络请求组合
swift
class UserService {
func loadFullProfile(userId: String) async throws -> FullProfile {
// 并发获取所有数据
async let userInfo = fetchUserInfo(userId)
async let posts = fetchUserPosts(userId)
async let friends = fetchUserFriends(userId)
async let preferences = fetchUserPreferences(userId)
// 等待所有结果
return try await FullProfile(
info: userInfo,
posts: posts,
friends: friends,
preferences: preferences
)
}
// 对比传统回调方式
func loadFullProfileOld(userId: String,
completion: @escaping (Result<FullProfile, Error>) -> Void) {
fetchUserInfo(userId) { result1 in
switch result1 {
case .success(let userInfo):
self.fetchUserPosts(userId) { result2 in
switch result2 {
case .success(let posts):
// 更多嵌套...
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
4.2 限制并发数量
swift
func downloadMultipleFiles(urls: [URL], maxConcurrent: Int = 4) async throws -> [Data] {
// 使用 TaskGroup 控制并发数
return try await withThrowingTaskGroup(of: Data.self) { group in
var results: [Data] = []
results.reserveCapacity(urls.count)
// 分批处理,限制并发数
for index in urls.indices {
if group.taskCount >= maxConcurrent {
// 等待一个任务完成再添加新的
if let result = try await group.next() {
results.append(result)
}
}
group.addTask {
return try await downloadFile(from: urls[index])
}
}
// 收集剩余结果
for try await result in group {
results.append(result)
}
return results
}
}
第五部分:与系统框架的集成
5.1 iOS 13+ 的系统 API 更新
swift
// iOS 13+ 提供了异步版本的 openURL
func openSettings() async -> Bool {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return false
}
return await UIApplication.shared.open(url)
}
// 使用示例
Task {
let success = await openSettings()
print("设置应用打开\(success ? "成功" : "失败")")
}
// 为什么使用Task?
// ❌ 错误:不能在同步函数中直接使用 await
func buttonTapped() {
let success = await openSettings() // 编译错误!
print("结果: \(success)")
}
// ✅ 正确:需要 Task 包装
func buttonTapped() {
Task { // 创建异步执行环境
let success = await openSettings()
print("结果: \(success)")
}
}
5.2 适配旧版本系统
swift
// 为 iOS 13+ 提供兼容方案
func openURL(_ url: URL) async -> Bool {
if #available(iOS 13.0, *) {
return await UIApplication.shared.open(url)
} else {
// 使用 continuation 桥接到 async/await
return await withCheckedContinuation { continuation in
UIApplication.shared.open(url) { success in
continuation.resume(returning: success)
}
}
}
}
第六部分:最佳实践总结
6.1 代码组织原则
- 优先使用 async/await 替代回调
- 合理使用 async let 进行并发,但注意数量控制
- 使用 Actor 保护共享状态,避免手动锁
- 尽量使用值类型,减少共享可变状态
6.2 架构设计建议
swift
// 推荐的层次结构:
// UI层 (@MainActor) - 处理用户交互和界面更新
// 业务层 (混合) - 协调数据流,处理业务逻辑
// 数据层 (async/await) - 网络请求、数据库操作
// 工具层 (值类型) - 纯函数计算、数据处理
@MainActor
class ViewController: UIViewController {
private let viewModel: UserViewModel
func loadData() async {
await viewModel.loadUserData()
updateUI()
}
}
actor UserViewModel {
private let repository: UserRepository
func loadUserData() async {
let user = await repository.fetchUser()
// 处理业务逻辑
}
}
class UserRepository {
func fetchUser() async throws -> User {
// 数据层操作
return try await apiClient.fetchUser()
}
}
第七部分:内部原理机制
Swift的async/await基于协程实现: 技术关系:
js
// 1个线程上可以运行多个协程
Thread A: [协程1运行] → [协程2运行] → [协程1恢复] → [协程3运行]
↑ ↑ ↑ ↑
遇到await挂起 遇到await挂起 结果返回恢复 遇到await挂起
// 协程在挂起时释放线程,让其他协程使用
// 传统线程 vs 协程
// 线程:操作系统调度,上下文切换成本高
Thread 1: [运行] → [阻塞等待I/O] → [运行]
Thread 2: [等待] → [运行] → [等待]
// 协程:用户态调度,轻量级
协程 A: [运行] → [挂起] → [运行]
协程 B: [运行] → [挂起]
// 在同一线程上交替执行,没有线程切换开销
结合实际代码说明:
Swift
// 规则1:一个协程必须在一个线程上运行
// 规则2:协程只能在特定点挂起(await处)
// 规则3:挂起的协程不占用线程
// 示例:
func fetchMultipleResources() async {
// 开始:在主线程运行(如果从@MainActor调用)
let data1 = await fetchData() // 挂起点1
// 挂起:释放主线程,其他协程可用
// 恢复:可能在任意线程(不一定是主线程)
process(data1) // 在某个后台线程执行
let data2 = await fetchData() // 挂起点2
// 再次挂起...
// 最后如果需要更新UI,要确保在主线程
await MainActor.run {
updateUI(data1, data2)
}
}
结语
Swift 的现代并发模型代表了并发编程的范式转变:
- 从手动调度到智能调度 - 信任运行时做出最优决策
- 从回调地狱到线性代码 - 使用 async/await 简化异步流程
- 从容易出错到内存安全 - 通过 Actor 和值语义避免数据竞争
- 从复杂管理到结构化 - 自动处理任务生命周期和取消
虽然学习曲线比 GCD 更陡峭,但一旦掌握,你将能编写出更安全、更简洁、更高效的并发代码。
进一步学习资源: