为什么学习Swift并发 ? 在移动应用开发中,并发编程是提升用户体验的关键技能。掌握 Swift 并发,让我们的应用更流畅、更高效!
📚 学习路径概览
- 🔰 初级知识
- ⚡ 中级知识
- 🚀 高级知识
- 🎯 实战应用
- 💡 常见陷阱
🔰 初级知识 - 打好基础
🎯 并发编程基础概念
并发 vs 并行
- 并发(Concurrency) :程序能同时处理多个任务(任务切换,宏观同时)。
- 并行(Parallelism) :多个任务在多个CPU核上真正同时执行。
并发就像餐厅服务员同时服务多桌客人(来回切换),并行就像多个服务员同时服务不同客人。
🤔 为什么需要并发?
- ✅ 提高应用响应性(UI不卡顿)
- ✅ 充分利用多核CPU
- ✅ 提升资源利用率
🆕 Swift 并发模型简介
Swift 5.5 引入了协程(Coroutine) 和结构化 并发概念。主要依赖
async/await- 异步编程的核心Task****- 任务管理Actor- 数据隔离
结构化任务使用async let或任务组创建,而非结构化任务则使用 Task 或 Task.detached 创建。结构化任务会像局部变量一样,一直存活到声明它的作用域结束为止,并且在超出作用域时会自动取消,从而明确了任务的存活时长。
⚡ async/await 语法体系
🔑 关键字与语法
async:声明异步函数await:等待异步操作完成throws/try:异步函数也可抛出错误
func fetchData() async throws -> Data {
// 异步操作
return Data()
}
func processData() async {
do {
let data = try await fetchData()
// 处理数据
} catch {
print("错误:(error)")
}
}
🎭 使用场景
网络请求、磁盘IO、耗时计算等
UI层调用异步数据加载
⚠️ 注意事项
- ❌ 只能在异步上下文中使用
await - ❌ 同步函数不能直接调用异步函数
- ✅ 异步函数可以调用同步函数
🔄 异步序列(AsyncSequence)
🎯 核心概念
AsyncSequence、AsyncIteratorProtocol
用于异步流式数据处理(如网络流、文件流)
struct Counter: AsyncSequence {
let limit: Int
struct AsyncIterator: AsyncIteratorProtocol {
let limit: Int
var current = 0
mutating func next() async -> Int? {
guard current < limit else { return nil }
current += 1
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limit: limit)
}
}
这段代码定义了一个名为 Counter 的结构体,它实现了 Swift 的 AsyncSequence 协议。在使用时可以用 for await 来异步遍历。
AsyncIterator实现了 AsyncIteratorProtocol 协议,用于生成序列中的下一个元素。其中next()异步迭代器的核心方法,每次调用会返回下一个计数值,直到达到 limit。
使用方法
for await number in Counter(limit: 5) {
print("计数:(number)")
}
🎬 典型场景
网络数据流、异步文件读取、实时数据处理
⚠️ 注意事项
- ❌
for await只能在异步上下文中使用 - ✅ 异步序列适合处理流式、分批到达的数据
⚡ 中级知识 - 进阶技能
🎯 Task 详解 - 异步任务的核心
🔑 基础语法与用法
-
创建异步任务
Task { ... }:
let task = Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
return "任务完成"
}
// 带返回值的任务
let dataTask = Task<String, Error> {
let data = try await fetchDataFromNetwork()
return data
}
Task { ... } 是一创建就开始执行的
-
获取任务结果
await task.value:
let result = await task.value
// 处理可能抛出的错误
do {
let data = try await dataTask.value
print("获取到数据:(data)")
} catch {
print("任务失败:(error)")
}
3. #### 取消任务 task.cancel():
// 取消任务
task.cancel()
// 检查任务是否被取消
if task.isCancelled {
print("任务已被取消")
}
4. #### 检测取消状态 Task.isCancelled:
let task = Task {
// 在任务内部检查取消状态
guard !Task.isCancelled else {
print("任务被取消,提前退出")
return
}
// 执行任务逻辑
try await performHeavyWork()
}
关于取消任务和检查取消状态,这里再做一个深入的了解,因为这两个还是会经常被使用到的。
一个任务想要取消,只有两种操作:
- 任务自身的 isCancel 设置为 true
- 在结构化并发任务: 如果任务有子任务,对子任务调用 cancel
至于子任务是否 cancel,取决于子任务本身对 isCancel 的检查. 取消的方式可以通过
- 检查 isCancel 的方式,返回正常兜底值
- 执行 checkCanceled 的方式,抛出错误, 由顶层 Task 的创建者的所在块进行捕获.
举个复杂例子
import Foundation
func work(_ text: String) async throws -> String {
var s = ""
for c in text {
// 读取父级Task状态
if Task.isCancelled {
print("Cancelled: (text)")
}
try Task.checkCancellation()
await Task.sleep(UInt64(1 * NSEC_PER_SEC))
print("Append: (c)")
s.append(c)
}
print("Done: (s)")
return s
}
@main
struct Test {
static func main() async {
do {
let value: String = try await withThrowingTaskGroup(of: String.self) { group in
// Task 1
group.addTask {
try await withThrowingTaskGroup(of: String.self) { inner in
// Task 1.1
inner.addTask { try await work("Hello") }
// Task 1.2
inner.addTask { try await work("World!") }
// 延迟 2.5 秒后取消 inner
await Task.sleep(UInt64(2.5 * Double(NSEC_PER_SEC)))
inner.cancelAll()
return try await inner.reduce([]) { $0 + [$1] }.joined(separator: " ")
}
}
// Task 2
group.addTask {
let result = try await work("Swift Concurrency")
return result
}
// 注意:reduce 在外层 group
return try await group.reduce([]) { $0 + [$1] }.joined(separator: " ")
}
print("Final value: (value)")
} catch {
print("Error: (error)")
}
}
}
| 时间 | Task 1.1 (Hello) | Task 1.2 (World!) | Task 2 (Swift Concurrency) | 事件 |
|---|---|---|---|---|
| 0.0s | Append: H (耗时1s) | Append: W (耗时1s) | Append: S (耗时1s) | 三个任务同时开始 |
| 1.0s | Append: e (耗时1s) | Append: o (耗时1s) | Append: w (耗时1s) | 继续执行 |
| 2.0s | Append: l (耗时1s) | Append: r (耗时1s) | Append: i (耗时1s) | 即将触发取消 |
| 2.5s | Cancelled: Hello抛 CancellationError | **Cancelled: World!**抛 CancellationError | 内层 group 调用 cancelAll()Task 1 的两个子任务终止 | |
| 2.6s | Task 1 捕获错误 → 向外抛 | Cancelled: Swift Concurrency抛 CancellationError | 外层 group 接收到错误Task 2 终止 |
知识点解析
Swift Concurrency 的取消是协作式的
- 任务并不会立刻停下来
- 必须在任务内部调用
Task.checkCancellation()或访问Task.isCancelled时,才会检测到取消并抛出CancellationError
取消是会向下传播的
inner.cancelAll()会取消内层任务组里的全部任务- 内层任务组如果抛错,外层任务组也会收到错误并自动取消剩余的任务
外层任务组出错 → 取消其他任务
- Task 1 出错后,外层 group 会取消 Task 2
- Task 2 在下一次
checkCancellation()时退出
-
设置优先级
Task(priority:):(如 .userInitiated, .background 等)
// 用户交互优先级(最高)
let userTask = Task(priority: .userInteractive) {
await handleUserTap()
}
// 用户发起优先级
let dataTask = Task(priority: .userInitiated) {
await fetchUserData()
}
// 实用工具优先级
let utilityTask = Task(priority: .utility) {
await processData()
}
// 后台优先级(最低)
let backgroundTask = Task(priority: .background) {
await cleanupCache()
}
任务优先级只是建议,不能保证一定被操作系统采纳
🎭 实际应用场景
📱 后台数据加载
class DataManager {
private var dataTask: Task<[User], Error>?
func loadUsers() async throws -> [User] {
// 取消之前的任务
dataTask?.cancel()
// 创建新任务
dataTask = Task(priority: .userInitiated) {
let users = try await fetchUsersFromAPI()
return users
}
return try await dataTask?.value ?? []
}
}
⏰ 定时任务与超时控制
func performTaskWithTimeout() async throws -> String {
let task = Task {
try await performLongRunningOperation()
return "操作完成"
}
let timeoutTask = Task {
try await Task.sleep(nanoseconds: 5_000_000_000) // 5秒超时
task.cancel()
throw TimeoutError()
}
// 等待任务完成或超时
do {
let result = try await task.value
timeoutTask.cancel() // 操作完成,取消超时任务
return result
} catch {
timeoutTask.cancel() // 超时任务先触发,也取消
throw error
}
}
🧮 并发计算与结果收集
func calculateFibonacciNumbers(_ count: Int) async -> [Int] {
// 批量创建任务
let tasks = (0..<count).map { index in
Task(priority: .utility) {
return fibonacci(index)
}
}
var results: [Int] = []
// 等待任务完成并收集结果
for task in tasks {
if let result = try? await task.value {
results.append(result)
}
}
return results.sorted()
}
上面的这种遍历循环的任务可以使用
AsyncSequence来实现哦
🚀 Task 的高级特性
-
Task 的生命周期管理
Task 的生命周期管理是并发编程中的关键概念,它确保任务不会无限期地占用系统资源,避免内存泄漏和资源浪费。
class TaskManager {
private var activeTasks: Set<Task<Void, Never>> = []
func startTask() {
let task = Task {
await performWork()
}
activeTasks.insert(task)
// 任务完成后自动清理
Task {
await task.value
activeTasks.remove(task)
}
}
func cancelAllTasks() {
activeTasks.forEach { $0.cancel() }
activeTasks.removeAll()
}
}
知识点
✅ 需要手动管理的场景
- 长期运行的任务管理器
- 需要动态添加/移除任务的场景
- 需要精确控制任务生命周期的场景
- 需要实现自定义任务队列的场景
❌ 不需要手动管理的场景:
- 结构化任务(使用 TaskGroup 或 async let)
- 一次性任务执行,局部Task
- 任务数量固定的并发场景
-
任务依赖关系
func processWithDependencies() async {
// 第一个任务:获取用户信息
let userTask = Task {
return try await fetchUserInfo()
}
// 第二个任务:基于用户信息获取订单
let orderTask = Task {
let user = try await userTask.value
return try await fetchOrders(for: user.id)
}
// 第三个任务:处理订单数据
let processTask = Task {
let orders = try await orderTask.value
return await processOrders(orders)
}
// 等待所有任务完成
let result = await processTask.value
print("处理完成:(result)")
}
知识点:
- 隐式依赖链:
orderTask依赖userTask,processTask依赖orderTask - 数据流传递:每个任务的结果作为下一个任务的输入参数
- 并发启动:所有任务可以同时启动,但执行时会自动等待依赖完成
- 错误传播:如果
userTask失败,后续任务会自动取消
-
任务取消传播
任务取消传播是结构化并发的核心特性,它确保当父任务被取消时,所有子任务也会被自动取消,避免资源泄漏和无效计算。
在上面的复杂例子中有体现,
知识点
- 结构化并发:子任务在父任务的作用域内创建,形成层次结构
- 自动取消传播:父任务取消时,所有子任务自动取消,无需手动管理
- 资源清理:取消传播确保所有相关资源得到及时释放
- 协作式取消:子任务需要主动检查
Task.isCancelled来响应取消
🔍 Task 与其他并发特性的对比
| Task | TaskGroup | async let | |
|---|---|---|---|
| 用途 | 单个异步任务 | 多个相关任务 | 多个独立任务 |
| 生命周期 | 手动管理 | 自动管理 | 自动管理 |
| 取消控制 | 精确控制 | 组级别控制 | 自动传播 |
| 适用场景 | 复杂任务逻辑 | 批量处理 | 简单并发 |
| 错误处理 | 灵活 | 统一 | 简单 |
🚀 TaskGroup - 并发任务组
🔑 关键字与用法
withTaskGroup:并发执行一组相关任务group.addTask { ... }:添加子任务for await result in group:收集结果
try await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { i * i }
}
for await value in group {
print(value)
}
}
🎬 典型场景
并发下载、批量数据处理
🛡️ 错误处理与取消
- ✅ 支持
try/catch,子任务抛错会传播到父任务 - ✅ 可在组内取消所有任务(
group.cancelAll())
⚠️ 注意事项
- 子任务间相互独立,不能直接通信
- 适合任务数量较多、耗时较长的场景
- 不要把 TaskGroup 的实例泄漏到外部,类似下面的写法。因为withTaskGroup 返回前会先等待所有的子 Task 执行完毕,然后将 TaskGroup 销毁。
guard let group = taskGroup else {
print("group is nil")
return
}
- 也不要在子任务中修改taskgroup,子Task 的执行体可能会被调度到不同的线程上,这样就导致对 TaskGroup 的修改是并发的,不安全。
await withTaskGroup(of: Void.self) { (group) -> Void in
group.addTask {
group.addTask { // error!
print("inner task")
}
}
}
⚡ async let - 并发声明
🔑 关键字与用法
async let:声明并发异步变量,自动并发执行,await等待结果
async let a = fetchA()
async let b = fetchB()
let result = await (a, b)
🎯 适用场景
- 🔄 多个独立异步操作并发执行
- ⚖️ 比 TaskGroup 更轻量,适合少量并发
⚠️ 注意事项
async let变量作用域为当前函数- 不能跨函数传递
🎭 Actor 模型基础 - 数据隔离的核心
Actor 是 Swift 并发编程中实现数据隔离的核心机制,它通过串行队列确保同一时间只有一个任务能访问 Actor 的内部状态,从而避免数据竞争
🔑 关键字与用法
actor:声明Actor类型- Actor内部状态自动隔离,线程安全
- 通过
await访问Actor方法
actor Counter {
private var value = 0
func increment() { value += 1 }
func getValue() -> Int { value }
}
let counter = Counter()
await counter.increment()
🎯 实际应用场景
共享状态管理、计数器、缓存等
🚀 Actor 的工作原理
-
内部机制
// Actor 的内部实现原理(简化版)
actor BankAccount {
private var balance: Double = 1000.0
private var transactions: [Transaction] = []
func deposit(_ amount: Double) {
balance += amount
transactions.append(Transaction(type: .deposit, amount: amount))
}
func withdraw(_ amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
transactions.append(Transaction(type: .withdraw, amount: amount))
return true
}
func getBalance() -> Double {
return balance
}
}
知识点
- 串行队列:每个 Actor 都有一个内部的串行队列
- 状态隔离:
balance和transactions只能在 Actor 内部直接访问 - 方法调用:所有外部调用都会进入队列,按顺序执行
- 数据一致性:确保
balance和transactions始终保持同步
-
并发访问示例
func testConcurrentAccess() async {
let account = BankAccount()
// 创建多个并发任务
await withTaskGroup(of: Void.self) { group in
// 任务1:存款
group.addTask {
await account.deposit(100)
print("存款100完成")
}
// 任务2:取款
group.addTask {
let success = await account.withdraw(50)
print("取款50: (success ? "成功" : "失败")")
}
// 任务3:查询余额
group.addTask {
let balance = await account.getBalance()
print("当前余额: (balance)")
}
}
}
技术要点
- 并发安全:多个任务可以同时调用 Actor 方法
- 顺序执行:虽然调用是并发的,但执行是串行的
- 结果一致性:无论执行顺序如何,最终状态都是一致的
- 性能影响:串行执行可能影响性能,但保证了数据安全
-
状态隔离与访问控制
actor UserProfile {
private var profile: Profile
private var lastUpdated: Date
// 只读属性:外部可以同步访问
nonisolated var displayName: String {
return profile.displayName
}
// 需要隔离的方法
func updateProfile(_ newProfile: Profile) {
profile = newProfile
lastUpdated = Date()
}
// 内部方法:不需要 await
private func validateProfile(_ profile: Profile) -> Bool {
return !profile.displayName.isEmpty
}
}
技术要点
nonisolated:标记不需要隔离的方法/属性,可以同步访问- 私有方法:Actor 内部的方法调用不需要 await
- 状态一致性:所有状态修改都通过隔离方法进行
- 性能优化:只读属性可以避免不必要的异步调用
⚠️ 注意事项
- Actor方法默认是异步的
- 不能直接跨Actor同步访问
- Actor之间通信需通过异步消息
🎨 MainActor 基础
🔑 关键字与用法
@MainActor:标记类/方法在主线程执行
用于UI更新、主线程安全
@MainActor
class ViewModel: ObservableObject {
@Published var text: String = ""
func updateText(_ newText: String) {
text = newText
}
}
🎯 典型场景
SwiftUI、UIKit界面更新
⚠️ 注意事项
@MainActor保证所有标记方法/属性在主线程访问- 避免在主线程执行耗时操作
🚀 高级知识 - 专家级技能
🎭 Actor 高级特性
-
继承与限制
Actor可继承(Swift 5.9+),但有并发限制
actor BaseActor {
var counter = 0
func sayHello() {
print("Hello from base, counter: (counter)")
}
}
actor ChildActor: BaseActor {
override func sayHello() {
print("Hello from child")
}
}
不能直接同步调用其他Actor方法
func test() async {
let child = ChildActor()
// ❌ 直接访问存储属性会报错(跨 actor 隔离)
// print(child.counter)
// ✅ 必须通过 await 调用 actor 的方法
await child.sayHello()
await child.sayHello()
}
Swift 的 actor 本质上是引用类型 + 隔离队列。
所谓并发限制:即使 ChildActor 继承了 BaseActor,但它们:
- 共享父类里的 actor 隔离规则
- 不能跨 actor 隔离直接访问父类存储属性
- 调用方法时依然需要遵守
await规则
-
Actor 之间的通信
actor OrderProcessor {
private var orders: [Order] = []
func addOrder(_ order: Order) {
orders.append(order)
}
func processOrders() async {
for order in orders {
// 调用其他 Actor 的方法
await inventoryManager.reserveItems(for: order)
await paymentProcessor.processPayment(for: order)
}
}
}
actor InventoryManager {
private var inventory: [String: Int] = [:]
func reserveItems(for order: Order) async -> Bool {
// 库存检查逻辑
return true
}
}
actor PaymentProcessor {
private var inventory: [String: Int] = [:]
func processPayment(for order: Order) async -> Bool {
// 库存检查逻辑
return true
}
}
技术要点
- 跨 Actor 调用:必须使用
await,因为可能涉及队列等待 - 消息传递:Actor 之间通过异步方法调用进行通信
- 状态隔离:每个 Actor 维护自己的状态,不会相互干扰
- 并发安全:多个 Actor 可以同时处理不同的请求
-
数据竞争问题
多线程同时读写同一变量,导致结果不可预测
class Counter {
private var count = 0
func increment() { count += 1 }
func getCount() -> Int { count }
}
func testDataRace() async {
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask { counter.increment() }
}
}
print("最终计数:(counter.getCount())") // 可能不是1000
}
✅ 解决方案
使用Actor(推荐)
使用锁(NSLock、DispatchQueue)
使用原子操作(OSAtomic)
actor SafeCounter {
private var count = 0
func increment() { count += 1 }
func getCount() -> Int { count }
}
🔍 Actor vs 其他并发安全方案
| Actor | NSLock | DispatchQueue | @MainActor | |
|---|---|---|---|---|
| 线程安全 | ✅ 自动 | ✅ 手动 | ✅ 手动 | ✅ 主线程 |
| 性能 | 🟡 中等 | 🟢 高 | 🟢 高 | 🟡 中等 |
| 易用性 | 🟢 简单 | 🟡 中等 | 🟡 中等 | 🟢 简单 |
| 灵活性 | 🟡 中等 | 🟢 高 | 🟢 高 | 🟡 中等 |
| 内存安全 | 🟢 自动 | 🟡 手动 | 🟡 手动 | 🟢 自动 |
🚫 死锁预防与内存管理
🚨 死锁场景
嵌套Actor调用、资源互锁
actor ResourceA {
var resourceB: ResourceB?
func accessB() async {
guard let b = resourceB else { return }
await b.doSomething()
}
}
actor ResourceB {
var resourceA: ResourceA?
func doSomething() async {
guard let a = resourceA else { return }
await a.accessB() // ❌ 死锁风险
}
}
Swift 的每个
actor都有自己的串行执行队列,任何访问它隔离域的代码,都必须进入该队列。
🛡️ 预防方法
- 使用弱引用缓存、及时释放资源
weak var resourceB: ResourceB?
2. 使用 nonisolated 方法,如果某些方法不需要访问 actor 隔离的数据,可以声明为:
nonisolated func doSomethingPublic() {
// ...
}
⚡ 性能优化技巧
📊 Task数量与批量处理
避免创建过多Task,采用批量分组
func efficientProcessing() async {
let items = Array(1...10000)
let batchSize = 100
await withTaskGroup(of: Void.self) { group in
for i in stride(from: 0, to: items.count, by: batchSize) {
let end = min(i + batchSize, items.count)
let batch = Array(items[i..<end])
group.addTask {
for item in batch {
processItem(item)
}
}
}
}
}
上面的代码使用结构化并发的 withTaskGroup,自动等待所有子任务完成。把 items 分成每 100 个为一组的 batch。每组数据都通过 group.addTask { ... } 放入一个并发子任务中执行。每个子任务中顺序执行本组的 100 个 item。这样处理之后,1万个任务切成100个批次,只创建了100个并发任务。
🎯 优先级管理
合理设置Task优先级,保障用户体验
func priorityManagement() async {
let userTask = Task(priority: .userInteractive) { await handleUserTap() }
let dataTask = Task(priority: .userInitiated) { await processUserData() }
let cleanupTask = Task(priority: .background) { await cleanupCache() }
await userTask.value
await dataTask.value
await cleanupTask.value
}
✅ 内存优化
使用弱引用、缓存清理
actor OptimizedCache {
private var cache: [String: WeakReference<Data>] = [:]
func store(_ data: Data, for key: String) { cache[key] = WeakReference(data) }
func retrieve(for key: String) -> Data? { cache[key]?.value }
func cleanup() { cache = cache.filter { $0.value.value != nil } }
}
class WeakReference<T: AnyObject> {
weak var value: T?
init(_ value: T) { self.value = value }
}
上面的代码实现了一个自动清理失效对象的“弱引用缓存”容器。用于防止缓存强引用对象,避免内存泄露
并发线程模型总结:
Concurrency 的本质
Swift Concurrency 并发在底层使用的是一种新实现的协同式线程池 (cooperative thread pool) 的调度方式: 由一个串行队列负责调度工作,它将函数中剩余的运行内容被抽象为更轻量的续体 (continuation),来进行调度
传统的 GCD 模式的局限性
在 Concurrency 库出现之前,Swift 是采用的 GCD 方式,将不同 thread 运行在不同内核上,可能出现多个 thread 在一个内核上运行的 case,单个线程的内存占用达到 500KB - 1MB。因此线程创建本身也是一件带有内存消耗的事情,在并发 queue 中如果任务未完成执行,那么 queue 会倾向于重新创建 thread,直到达到并发队列的上限(64) 为止。
🎯 实战应用 - 真实场景
📤 场景一:意见反馈日志多文件上传
需要同时上传多个文件,每个文件上传可能成功或失败,需要记录详细日志。
actor File {
let fileName: String
private var uploadError: Error? = nil
init(fileName: String) {
self.fileName = fileName
}
func setUploadError(_ error: Error) {
self.uploadError = error
}
func hasUploadError() -> Bool {
return uploadError != nil
}
}
class Logger: @unchecked Sendable {
func log(level: String, msg: String) {
print("[(level)] (msg)")
}
}
actor Uploader {
let logger = Logger()
func upload(multiFile: File) async {
let fileName = multiFile.fileName
logger.log(level: "info", msg: "开始上传多文件: (fileName)")
let randomSeconds = Int.random(in: 2...5)
let delay = UInt64(randomSeconds) * 1_000_000_000 // 转换为纳秒
try? await Task.sleep(nanoseconds: delay)
if Bool.random() {
await multiFile.setUploadError(NSError(domain: "UploadError", code: 1, userInfo: nil))
}
}
func startUploading(files: [File]) async {
await withTaskGroup(of: Void.self) { group in
for file in files {
group.addTask {
await self.upload(multiFile: file)
let fileName = file.fileName
let hasError = await file.hasUploadError()
if hasError {
self.logger.log(level: "error", msg: "文件(fileName)上传失败,上传流程中断")
} else {
self.logger.log(level: "info", msg: "文件(fileName)上传流程完成")
}
}
}
self.logger.log(level: "info", msg: "等待所有上传任务执行完成")
await group.waitForAll()
self.logger.log(level: "info", msg: "所有上传任务执行完成")
}
self.logger.log(level: "info", msg: "所有文件上传流程完成")
}
}
let uploader = Uploader()
let files = [
File(fileName: "file1.txt"),
File(fileName: "file2.txt"),
File(fileName: "file3.txt"),
File(fileName: "file4.txt"),
File(fileName: "file5.txt"),
File(fileName: "file6.txt"),
File(fileName: "file7.txt"),
File(fileName: "file8.txt")
]
Task {
await uploader.startUploading(files: files)
print("所有日志上传文件结束")
}
对比一下以前的写法
💡 关键知识点
@unchecked Sendable
@unchecked Sendable是一个属性包装器,表示某个类或结构体是 Sendable,允许在并发任务中安全地跨线程传递。Sendable确保对象可以在多个线程间传递而不发生数据竞争。Logger类被标记为@unchecked Sendable,表示它可以在多个并发任务中传递。在 Swift 并发模型中,如果某个对象被标记为@unchecked Sendable,意味着该对象的 线程安全性 没有被检查,所以需要我们非常注意,这个被标记的类是否真的是线程安全,这里Logger只是一个打印类,所以可以被标记
withTaskGroup
Task Group 用于将多个异步任务组合成一个并发任务组,并且可以等待所有任务完成。任务组让多个异步任务并发执行,且可以等待所有任务完成。
在 Uploader 的 startUploading 方法中,使用了 withTaskGroup 来并行上传多个文件,并在所有任务完成后输出最终日志。
🔄 场景二:串行异步任务队列
有时候经常需要在不同地方将数据插入数据库,通过TaskQueue来管理加入的任务,让他们按顺序插入,防止数据竞争
/// 利用actor达到防止插入数据库的数据资源竞争问题
actor TaskQueue {
private var lastTask: Task<Void, Never>?
func addTask(_ task: @escaping () async -> Void) {
let previousTask = lastTask // 复制 lastTask,防止 actor 内部 await 自己
let newTask = Task {
// 在 actor 外部等待上一个任务完成,避免死锁
await previousTask?.value
await task()
}
lastTask = newTask
}
}
// 全局队列
let taskQueue = TaskQueue()
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 在多个地方调用
Task { await insertData("数据 1") }
}
// 模拟异步插入数据库函数
func insertData(_ value: String) async {
await taskQueue.addTask {
print("开始插入数据: (value) at (Date())")
try? await Task.sleep(nanoseconds: 2_000_000_000) // 模拟数据库插入
print("完成插入数据: (value) at (Date())")
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task { await insertData("数据 2") }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task { await insertData("数据 3") }
}
}
💡 关键知识点
actor
- 线程隔离:
actor会自动确保它的内部状态只能被 一个任务 同时访问和修改。通过这种方式,actor可以保证在并发环境下不会发生 数据竞争 和 资源冲突。 - 异步执行:
actor的方法通常是 异步的,并且 需要通过await关键字来访问或修改其状态,这意味着它们会在异步上下文中执行,防止阻塞其他操作。
在上面的代码中,在 addTask 方法中,我们复制了 lastTask,并创建了一个新的 Task。每当有新任务加入时,它会等待读取 上一个任务 的结果(通过 await previousTask?.value),这就实现了 任务的串行执行。新任务只有在前一个任务完成后才会开始执行。
防止死锁:在 TaskQueue 内部,我们通过复制lastTask 来避免 actor 内部直接 await 自己的任务,这样可以防止在任务链中发生死锁。如果直接调用 await lastTask?.value,可能会导致任务的循环等待,从而产生死锁。
🧵 场景三:Task 内部线程问题
现在,只要是系统的UI组件,都默认加了@MainActor标志,同时 @Published , @State , @Binding 等属性包装器在swift6的强制模式下,都要求必须在主线程调用,否则会报错
这也就意味着,只要是外部调用了带有@MainActor标志的类的函数或者属性,默认都是在主线程执行的。
class ViewController: UIViewController {
var viewModel = ViewModel()
var label: UILabel!
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 设置 UILabel
label = UILabel()
label.frame = CGRect(x: 20, y: 100, width: 300, height: 40)
label.text = "Loading..."
view.addSubview(label)
test1()
}
}
extension ViewController {
func test1() {
viewModel.$data
.receive(on: DispatchQueue.main)
.sink(receiveValue: { text in
self.label.text = text
})
.store(in: &cancellables)
Task {
print("开始查询: (Thread.current)")
await viewModel.fetchData() // 异步调用 ViewModel 的网络请求
await viewModel.fetchData1() // 异步调用 ViewModel 的网络请求
// label.text = viewModel.data // ❌
print("查询结束: (Thread.current)")
}
}
}
class ViewModel: ObservableObject {
@Published var data: String = "Loading..."
func fetchData() async {
Task {
print("Start fetching data on thread: (Thread.current)")
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 模拟耗时操作 1 秒
print("Data fetched on thread: (Thread.current)")
// await MainActor.run {
self.data = "Failed to load data"
// }
}
}
func fetchData1() async {
Task.detached {
print("Start fetching data on thread: (Thread.current)")
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 模拟耗时操作 1 秒
print("Data fetched on thread: (Thread.current)")
await MainActor.run {
self.data = "Failed to load data"
}
}
}
}
- 尝试去掉fetchData()函数的 Task 看看效果
- 尝试给test1包一个
DispatchQueue.global().async看看效果
💡 关键知识点
Task 和Task.detached
Task它允许我们异步地执行某个操作,并自动将其调度到适当的执行上下文中(默认情况下是在调用它的线程上下文中执行,通常是主线程)。Task.detached:它允许在完全独立的上下文中执行任务,不受调用上下文的影响,通常用于后台操作。
Context
关于上下文的理解,官方文档还是一如既往的描述少的可怜。
Swift 并发中所谓的「上下文」,并不等于线程上下文(即你在哪个线程调用) ,而是任务调度上下文(包括 actor、任务父子关系等) 。
-
结构化任务上下文
- 当前是不是在某个 Task 的作用域内,它会继承父任务的调度策略
-
actor 上下文
- 如果在某个 actor 中调用,它会绑定到该 actor 的隔离上下文
-
MainActor
- 调度在主线程
-
当前调用线程
- 仅作为一个调度 hint,不能完全决定 Task 执行线程
这里还有一些迷惑的点,如果没有父Task,那么他的调度策略是啥样的???不知道,完全由系统决定,可能是主线程,也可能是子线程
可以确定的是
Task.detached创建的分离任务执行的线程基本是非主线程。
🚫 场景四:在 Task 内部使用信号量导致死锁
有不少Task下执行的函数内部还有Dispatch的信号量。
当这个函数只执行一次的时候是没有问题的,这里我将逻辑抽离成DailyInsightRepository
DispatchQueue.global().async {
DailyInsightRepository().testDeadlockWithTask()
}
class DailyInsightRepository: NSObject {
// 模拟一个需要等待的网络请求函数
func fetchDailySleepUpdateIndentity(on dayString: String, completion: @escaping (String?) -> Void) {
// 模拟一个网络请求的延迟
DispatchQueue.global().async {
print("Fetching sleep data for (dayString) on (Thread.current)")
// 模拟延迟1秒
sleep(10)
// 返回一个睡眠标识符
completion("sleepIdentifierFor(dayString)")
}
}
// 需要通过信号量来等待网络请求的函数
func needUpdateCache(dayString: String) -> String {
let semaphore = DispatchSemaphore(value: 0)
var nowIdentifier: String?
// 在后台执行模拟的网络请求
fetchDailySleepUpdateIndentity(on: dayString) { identifier in
nowIdentifier = identifier
semaphore.signal() // 一旦网络请求完成,释放信号量
}
// 阻塞当前线程,直到信号量被释放
semaphore.wait()
// 如果没有获取到有效的标识符,返回 "noData"
guard let identifier = nowIdentifier else {
return "noData"
}
return "Fetched data with identifier: (identifier)"
}
// 使用 Task 来调用 needUpdateCache
func testDeadlockWithTask() {
Task {
print("Task started on (Thread.current)")
// 调用 needUpdateCache,模拟异步任务中的死锁问题
let result = needUpdateCache(dayString: "2023-08-01")
// 打印结果
print("Result: (result)")
print("Task finished on (Thread.current)")
}
}
}
但是当并发执行的时候,这里的Task因为继承了上下文,可能就会在同一个线程中产出互相等待,从而造成死锁
DispatchQueue.global().async {
self.repo.testDeadlockWithTask()
self.repo.testDeadlockWithTask()
}
class DailyInsightRepository: NSObject {
// 模拟一个需要等待的网络请求函数
func fetchDailySleepUpdateIndentity(on dayString: String, completion: @escaping (String?) -> Void) {
// 模拟一个网络请求的延迟
DispatchQueue.global().async {
print("Fetching sleep data for (dayString) on (Thread.current)")
// 模拟延迟1秒
sleep(10)
// 返回一个睡眠标识符
completion("sleepIdentifierFor(dayString)")
}
}
// 需要通过信号量来等待网络请求的函数
func needUpdateCache(dayString: String) -> String {
print("needUpdateCache (Thread.current)")
let semaphore = DispatchSemaphore(value: 0)
var nowIdentifier: String?
// 在后台执行模拟的网络请求
fetchDailySleepUpdateIndentity(on: dayString) { identifier in
nowIdentifier = identifier
semaphore.signal() // 一旦网络请求完成,释放信号量
}
// 阻塞当前线程,直到信号量被释放
semaphore.wait()
// 如果没有获取到有效的标识符,返回 "noData"
guard let identifier = nowIdentifier else {
return "noData"
}
return "Fetched data with identifier: (identifier)"
}
// 使用 Task 来调用 needUpdateCache
func testDeadlockWithTask() {
Task {
print("Task started on (Thread.current)")
// 调用 needUpdateCache,模拟异步任务中的死锁问题
let result = needUpdateCache(dayString: "2023-08-01")
// 打印结果
print("Result: (result)")
print("Task finished on (Thread.current)")
}
}
}
✅ 解决方案:使用 async/await
第一种,就是使用 testDeadlockWithTask 每次调用都是用一个Task包裹,
Task {
self.repo.testDeadlockWithTask()
}
Task {
self.repo.testDeadlockWithTask()
}
但是这样的调用,在其内部实际上是串行执行,并非并发执行
第二种方案:使用 async/await 替代信号量
class DailyInsightRepository1: NSObject {
// 模拟一个需要等待的网络请求函数
func fetchDailySleepUpdateIndentity(on dayString: String) async -> String? {
// 模拟一个网络请求的延迟
print("Fetching sleep data for (dayString) on (Thread.current)")
// 模拟延迟1秒
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
// 返回一个睡眠标识符
return "sleepIdentifierFor(dayString)"
}
// 使用 async/await 来改进 needUpdateCache 函数
func needUpdateCache(dayString: String) async -> String {
print("needUpdateCache (Thread.current)")
// 异步获取睡眠标识符
if let nowIdentifier = await fetchDailySleepUpdateIndentity(on: dayString) {
return "Fetched data with identifier: (nowIdentifier)"
}
return "noData"
}
// 使用 Task 来调用 needUpdateCache
func testDeadlockWithTask() {
Task {
print("Task started on (Thread.current)")
// 调用 needUpdateCache,模拟异步任务中的死锁问题
let result = await needUpdateCache(dayString: "2023-08-01")
// 打印结果
print("Result: (result)")
print("Task finished on (Thread.current)")
}
}
}
优先推荐第二种方案,因为第二种方案在支持并发执行的情况下也不会有线程阻塞
💡 关键知识点
这个场景的知识点,其实和上面的几个场景类似,主要是Task的线程以及配合async和await相关的操作
💡 最佳实践与常见陷阱
🔑 并发中的关键字总结
在并发过程中常见的关键字如下:
async/await Task 、actor、TaskGroup、 @MainActor、 @Sendable 、 @nonisolated
其中大多数在上面的课程中已经说过。接下来简单介绍未触及到的关键字
nonisolated
提供非隔离访问,表示它不依赖于 actor 的状态, 即使在 actor 外部 也可以直接同步调用(无需 await)。
actor Logger {
// 非隔离属性(常量),可以同步访问
nonisolated let version = "1.0.0"
// 非隔离方法:不访问任何 actor 状态,可以在 actor 外部直接调用
nonisolated func staticInfo() -> String {
"Logger version (version), created by ChatGPT"
}
// actor 内部状态
private var logs: [String] = []
// 隔离方法,必须 await 才能访问
func log(_ message: String) {
logs.append("[(Date())] (message)")
}
func allLogs() -> [String] {
logs
}
}
// 🌟 调用者
func testLogger() {
let logger = Logger()
// ✅ nonisolated 方法/属性:可以同步调用,无需 await
print(logger.version)
print(logger.staticInfo())
// ❗️下面是 actor 隔离方法,必须 await 调用
Task {
await logger.log("App started")
await logger.log("User tapped button")
let logs = await logger.allLogs()
print("📃 日志内容:\n(logs.joined(separator: "\n"))")
}
}
@TaskLocal
@TaskLocal 是一个新引入的关键字,用于在任务之间共享数据。它允许你将某些状态与当前的任务绑定,而不是将其与全局状态绑定。@TaskLocal 可以确保每个任务都有自己的独立数据副本,避免任务之间的干扰。
import Combine
// 定义一个 TaskLocal 变量
enum TraceID {
@TaskLocal static var current: String?
}
func log(_ message: String) {
print("[(TraceID.current ?? "no-trace")] (message)")
}
func doWork() async {
log("任务开始")
await try? Task.sleep(for: .seconds(1))
log("任务结束")
}
struct MyApp {
static func main() async {
// 设置 TaskLocal 的值
await TraceID.$current.withValue("TRACE-12345") {
await doWork()
}
// 这里 TraceID.current 已经恢复默认
await doWork()
}
}
Task {
await MyApp.main()
}
@TaskLocal 修饰的属性可以确保每个任务在执行时都有自己的局部数据副本,避免共享全局状态。
🚨 常见陷阱与解决方案
-
死锁问题
- 原因:嵌套 Actor 调用、资源互锁
- 解决:使用弱引用、nonisolated 方法
-
数据竞争
- 原因:多线程同时读写同一变量
- 解决:使用 Actor、锁、原子操作
-
线程阻塞
- 原因:在 Task 中使用信号量
- 解决:使用 async/await 替代
-
内存泄漏
- 原因:强引用循环
- 解决:使用弱引用、及时清理
🎯 性能优化建议
- 合理使用 Task 数量:避免创建过多 Task,采用批量分组
- 优先级管理:合理设置 Task 优先级,保障用户体验
- 内存优化:使用弱引用、缓存清理
- 错误处理:及时处理异常,避免资源浪费
🚀 学习资源推荐
📚 官方文档
Swift Concurrency
🎥 视频教程
Meet async/await in Swift - WWDC21 - Videos - Apple Developer
Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer