前言
Swift 并发编程(Swift Concurrency)中,任务(Task)是执行异步代码的最小单元。Swift 提供了三种创建任务的方式:
- 结构化任务(Child Tasks)
- 非结构化任务(Unstructured Tasks)
- 分离任务(Detached Tasks)
本文将重点讲解 非结构化任务 和 分离任务,帮助你深入理解它们的区别、使用场景以及注意事项。
非结构化任务(Unstructured Tasks)
✅ 1. 什么是非结构化任务?
非结构化任务通过 Task { ... }
创建,它不会与调用它的上下文形成父子任务关系,因此 不参与结构化并发。这意味着:
- 它的生命周期独立于调用者。
- 调用者不需要等待它完成。
- 它不会自动继承结构化并发的取消行为。
✅ 2. 上下文继承
虽然非结构化任务不参与结构化并发,但它会继承以下上下文信息:
继承项 | 说明 |
---|---|
当前 Actor | 如果在 Actor 中创建任务,它会运行在该 Actor 上 |
Task Local 值 | 会继承当前任务的局部值 |
任务优先级 | 会继承当前任务的优先级 |
✅ 3. 示例讲解
示例 1:在 Actor 中使用非结构化任务
swift
actor SampleActor {
var someCounter = 0
func incrementCounter() {
Task {
someCounter += 1 // ✅ 安全访问 actor 内部状态
}
}
}
✅ 说明:虽然 Task
是异步的,但由于它在 Actor 内部创建,因此可以安全地访问 someCounter
,无需使用 await
。
示例 2:在 @MainActor 上下文中创建任务
swift
@MainActor
func fetchData() {
Task {
// ✅ 这个任务运行在 MainActor 上
let data = await fetcher.getData() // 不会阻塞主线程
self.models = data // ✅ 主线程更新 UI
}
}
✅ 说明:await
不会阻塞主线程,主线程在等待期间可以继续处理其他任务。
示例 3:fire-and-forget 风格日志函数
swift
func log(_ string: String) {
print("LOG:", string)
Task {
await uploadMessage(string) // ✅ 异步上传日志
print("message uploaded")
}
}
✅ 说明:调用 log
函数后无需等待上传完成,适合日志、埋点等场景。
分离任务(Detached Tasks)
✅ 1. 什么是分离任务?
分离任务通过 Task.detached { ... }
创建,它是非结构化任务的一种特殊形式,不继承任何上下文:
- 不继承当前 Actor(即使你在
@MainActor
中创建) - 不继承 Task Local 值
- 不继承任务优先级
✅ 2. 示例讲解
示例 1:在 Actor 中使用分离任务
swift
actor SampleActor {
var someCounter = 0
func incrementCounter() {
Task.detached {
// ❌ 错误:不能直接在非隔离上下文中访问 actor 状态
// someCounter += 1
await self.someCounter += 1 // ✅ 需要显式使用 await
}
}
}
⚠️ 说明:分离任务不在 Actor 上运行,因此访问 actor 状态必须通过 await
。
示例 2:在 @MainActor 中使用分离任务
swift
@MainActor
func fetchData() {
Task.detached {
// ✅ 运行在后台线程
let data = await fetcher.getData()
self.models = data // ❌ 注意:此时更新 UI 可能不在主线程
}
}
⚠️ 说明:分离任务不会运行在 MainActor 上,因此更新 UI 时需要注意线程安全。
非结构化任务 vs 分离任务对比
特性 | 非结构化任务(Task {}) | 分离任务(Task.detached) |
---|---|---|
是否参与结构化并发 | ❌ | ❌ |
是否继承 Actor | ✅ | ❌ |
是否继承 Task Local 值 | ✅ | ❌ |
是否继承优先级 | ✅ | ❌ |
是否推荐优先使用 | ✅ | ❌(仅在必要时使用) |
使用建议与最佳实践
✅ 使用非结构化任务的场景:
- 从同步函数中调用异步函数(如
viewDidLoad
中调用异步接口) - 实现 fire-and-forget 风格的操作(如日志上传)
- 在 Actor 或 MainActor 中执行异步任务,但仍希望继承上下文
⚠️ 使用分离任务的场景(谨慎):
- 明确希望脱离当前 Actor 和上下文
- 执行与当前任务完全无关的后台任务
- 不需要结构化并发的取消或等待机制
总结与扩展思考
✅ 总结:
Task {}
是最常用的方式,适用于大多数异步任务。Task.detached
是高级工具,仅在确实需要脱离上下文时使用。- 分离任务不会继承 Actor,因此访问 actor 状态时必须使用
await
。 - 非结构化任务不会阻塞调用者,适合异步后台处理。
扩展思考:
- 在 SwiftUI 中使用非结构化任务:
swift
struct ContentView: View {
@State private var data: String = ""
var body: some View {
Text(data)
.task {
// ✅ SwiftUI 提供的 task 修饰符会自动管理生命周期
data = await fetchData()
}
}
}
✅ 说明:SwiftUI 的 .task
会在视图消失时自动取消任务,适合生命周期绑定。
- 日志系统中的实际应用:
swift
class Logger {
func log(_ message: String) {
print("本地日志:", message)
Task {
await uploadLog(message)
}
}
private func uploadLog(_ message: String) async {
// 模拟网络上传
try? await Task.sleep(nanoseconds: 1_000_000_000)
print("日志上传完成:", message)
}
}
✅ 说明:调用 log
后立即返回,不阻塞主线程,适合埋点、日志收集等。
- 使用
TaskGroup
实现结构化并发
swift
await withTaskGroup(of: String.self) { group in
for i in 1...3 {
group.addTask {
return "任务 \(i) 完成"
}
}
for await result in group {
print(result)
}
}
✅ 说明:与 Task {}
不同,TaskGroup
提供结构化并发,适合并行任务管理。