多线程
一、什么是多线程?为什么要使用多线程?
多线程是指在同一个进程中同时执行多个线程,每个线程可并行处理不同任务。
使用多线程的目的:
- 提升应用响应速度,防止主线程卡顿(如网络请求、图片处理)。
- 充分利用多核 CPU,提高性能。
- 分离耗时任务与 UI 操作,提升用户体验。
注意:
线程开销较大,过度创建可能导致资源竞争、切换开销、死锁等问题。
二、Swift 中常用的多线程方案
1. GCD(Grand Central Dispatch)
系统级的并发框架,轻量高效,自动管理线程。
2. Operation / OperationQueue
GCD 的面向对象封装,支持依赖、取消、优先级等功能。
3. Thread
底层方式,手动管理线程生命周期,不推荐日常使用。
4. Swift Concurrency(async/await)
Swift 5.5 新引入语法,支持结构化并发、Task、Actor 模型,更安全直观。
三、GCD 的核心概念
1. 队列(DispatchQueue)
任务的排队执行容器:
- 串行队列(Serial):一次只执行一个任务。
- 并行队列(Concurrent):可同时执行多个任务。
2. 同步与异步
- sync:等待任务完成后继续执行。
- async:立即返回,不阻塞当前线程。
let queue = DispatchQueue(label: "com.example.test")
queue.async {
print("异步任务1")
}
queue.async {
print("异步任务2")
}四、主线程与主队列的区别
- 主线程(Main Thread):负责 UI 更新与事件响应。
- 主队列(DispatchQueue.main):运行在主线程上的串行队列。
示例:
DispatchQueue.main.async {
// 更新 UI
}五、GCD 的常见用法
1. 延迟执行
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("2秒后执行")
}2. 栅栏(barrier)实现线程安全写操作
let queue = DispatchQueue(label: "com.example.queue", attributes: .concurrent)
queue.async(flags: .barrier) {
// 写操作,互斥执行
}3. 一次性执行(once)
static let shared: MyManager = {
let instance = MyManager()
return instance
}()4. 任务组(DispatchGroup)
let group = DispatchGroup()
let queue = DispatchQueue.global()
group.enter()
queue.async {
print("任务1")
group.leave()
}
group.enter()
queue.async {
print("任务2")
group.leave()
}
group.notify(queue: .main) {
print("所有任务完成")
}六、Operation 与 OperationQueue
Operation 表示单个任务;OperationQueue 负责调度与执行。
优点:
- 支持任务依赖(addDependency)
- 可取消任务(cancel())
- 可监听状态(isExecuting, isFinished)
- 可设置优先级与并发数
let queue = OperationQueue()
let op1 = BlockOperation { print("任务1") }
let op2 = BlockOperation { print("任务2") }
op2.addDependency(op1)
queue.addOperations([op1, op2], waitUntilFinished: false)七、GCD 与 OperationQueue 区别
| 对比项 | GCD | OperationQueue |
|---|---|---|
| 编程风格 | C API | 面向对象 |
| 依赖关系 | 不支持 | 支持 addDependency |
| 取消任务 | 不支持 | 支持 cancel |
| 状态监听 | 无 | 支持 isFinished 等 |
| 可扩展性 | 低 | 高,可继承自定义 |
八、Swift Concurrency(async/await)
Swift 5.5 引入的结构化并发机制,使异步代码更易读。
优势:
- 写法接近同步逻辑,消除回调地狱。
- 自动线程调度与生命周期管理。
- 支持 Task、TaskGroup、Actor。
func fetchData() async throws -> String {
try await Task.sleep(nanoseconds: 1_000_000_000)
return "数据返回"
}
Task {
let result = try await fetchData()
print(result)
}九、死锁及避免方式
1. 死锁定义
多个线程互相等待对方释放资源,导致程序永久阻塞。
2. 常见死锁场景
let queue = DispatchQueue(label: "com.test")
queue.sync {
queue.sync { print("死锁") }
}原因: 同一串行队列内,外层任务未完成又同步等待自身。
3. 解决思路
- 避免在同一串行队列中使用 sync 嵌套。
- 合理使用异步调用。
- 谨慎使用锁,防止双重竞争。
十、线程安全的常见手段
1. 串行队列
一次只执行一个任务。
2. DispatchSemaphore(信号量)
控制同时访问资源的线程数。
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
semaphore.wait()
print("安全访问资源")
semaphore.signal()
}3. 锁机制
如 NSLock、RecursiveLock、os_unfair_lock 等。
4. DispatchBarrier
在并行队列中强制任务互斥执行。
十一、线程间通信方式
- 主线程回调:
DispatchQueue.global().async {
let data = fetchData()
DispatchQueue.main.async {
updateUI(data)
}
}- NotificationCenter:广播式通信。
- Delegate / Closure:一对一回调。
- Combine / AsyncStream:响应式数据流。
十二、常见扩展问法
Q1:GCD 的 global() 队列是什么?
系统提供的全局并行队列,适合执行通用异步任务。
DispatchQueue.global(qos: .userInitiated).async { ... }Q2:QoS(Quality of Service) 是什么?
任务优先级等级:
- .userInteractive:最高优先级,UI 相关。
- .userInitiated:用户期望立即得到结果。
- .utility:耗时任务,如下载。
- .background:最低优先级,后台同步。
十三、Swift Concurrency 深入:Task、Actor、AsyncStream
一、Task 的概念与用法
Task 是 Swift 并发体系的基本执行单元,用于启动一个异步任务。
1. 创建 Task
Task {
print("这是一个异步任务")
}等价于在 GCD 中:
DispatchQueue.global().async { ... }2. 带返回值的 Task
let task = Task {
return try await fetchData()
}
let result = try await task.value3. Task 的生命周期与取消
每个 Task 都可以被取消:
let task = Task {
for i in 1...5 {
try Task.checkCancellation()
print("执行中:\(i)")
}
}
task.cancel()注意:
Task.checkCancellation() 不会自动抛错,而是主动检测是否被取消。
二、TaskGroup(任务组)
任务组允许并发执行多个子任务,并在所有任务完成后统一返回。
1. 使用示例
func fetchAll() async throws -> [String] {
try await withThrowingTaskGroup(of: String.self) { group in
let urls = ["a.com", "b.com", "c.com"]
for url in urls {
group.addTask {
return try await fetchData(from: url)
}
}
var results = [String]()
for try await result in group {
results.append(result)
}
return results
}
}优势:
- 自动管理并发子任务。
- 子任务抛出错误时可统一捕获。
- 所有任务结束后自动清理资源。
三、Actor —— 线程安全的并发模型
Actor 是 Swift 的一种并发安全类型,类似于一个带锁的类,用于防止数据竞争。
1. 定义与使用
actor Counter {
var value = 0
func increment() {
value += 1
}
}
let counter = Counter()
Task {
await counter.increment()
}2. 为什么要用 Actor?
在传统多线程环境下,多个线程同时读写同一变量会造成数据不一致。
而 Actor 保证同一时间只能由一个任务访问其可变状态。
3. MainActor
@MainActor 表示该任务必须在主线程上执行,常用于 UI 更新。
@MainActor
func updateUI() {
// 一定在主线程执行
}四、AsyncStream —— 异步数据流
AsyncStream 用于在异步环境中逐步产生数据(类似 Combine 的 Publisher)。
1. 基本用法
func makeCounterStream() -> AsyncStream<Int> {
AsyncStream { continuation in
for i in 1...5 {
continuation.yield(i)
}
continuation.finish()
}
}2. 读取数据
Task {
for await value in makeCounterStream() {
print("收到数据:\(value)")
}
}3. 实际应用场景
- 网络请求分块下载
- 实时事件监听(如 Socket、传感器数据)
- UI 层监听异步回调流
五、AsyncLet 并发绑定
async let 可同时启动多个异步任务并在后续等待结果。
async let image = loadImage()
async let user = fetchUser()
async let posts = fetchPosts()
let (img, usr, pst) = await (image, user, posts)相比 TaskGroup,async let 更轻量,适合数量固定的异步任务。
六、Task 优先级(Priority)
Swift 并发系统中可以指定任务的优先级,类似 GCD 的 QoS。
| 优先级 | 描述 |
|---|---|
| .high | 用户交互级别任务 |
| .medium | 普通异步任务 |
| .low | 后台任务或非关键任务 |
Task(priority: .high) {
await doImportantWork()
}七、传统 GCD 与 Swift Concurrency 对比
| 特性 | GCD | Swift Concurrency |
|---|---|---|
| 语法风格 | 回调闭包 | async/await |
| 错误处理 | 手动回调 | try/throw 自动传播 |
| 依赖管理 | 无 | TaskGroup 管理 |
| 生命周期 | 程序员管理 | 系统自动管理 |
| 可读性 | 回调地狱 | 顺序逻辑,易读易维护 |
| 内存安全 | 需手动同步 | Actor 自动保证 |
八、在 iOS 实际项目中的应用场景
- 网络请求并发
async let user = fetchUser()
async let profile = fetchProfile()
async let feed = fetchFeed()
let (u, p, f) = await (user, profile, feed)-
可同时请求多接口,加速加载速度。
-
图片下载与缓存
使用 AsyncStream 或 TaskGroup 管理多个下载任务,避免重复下载。
-
UI 更新
配合 @MainActor,确保界面修改在主线程安全执行。
-
后台同步任务
使用低优先级 Task 或后台 actor 管理,节省资源。
九、面试延伸问题与思路
Q1:Swift Concurrency 与传统锁相比,性能如何?
A:Actor 内部自动调度访问,不使用传统锁,性能更高且避免死锁风险。
Q2:Task 与 GCD 的 async 区别?
A:Task 属于结构化并发,有生命周期和取消机制;GCD 是全局调度,缺少任务关系。
Q3:在 Actor 内调用非隔离函数会怎样?
A:需要 await 关键字,否则编译错误。Actor 强制同步点,确保数据一致性。
Q4:能否在 UIKit 中直接使用 async/await?
A:可以,从 iOS 15 开始原生支持。例如:
@MainActor
func updateUI() async {
label.text = "加载完成"
}十四、多线程面试常见陷阱与实战题(含代码分析)
一、经典陷阱题:GCD 死锁
题目:
print("1")
DispatchQueue.main.sync {
print("2")
}
print("3")结果与原因:
程序卡死(死锁)。
原因分析:
main.sync 会等待主队列中的任务执行完,但主队列本身当前就在执行这段代码,因此相互等待 → 死锁。
正确写法:
DispatchQueue.main.async {
print("2")
}输出顺序:1 → 3 → 2
二、异步与同步混用陷阱
题目:
let queue = DispatchQueue(label: "com.test")
print("A")
queue.async {
print("B")
queue.sync {
print("C")
}
print("D")
}
print("E")输出:
A
E
B
(死锁)原因:
queue.async 异步执行 B,接着内部 queue.sync 又在同一串行队列中同步等待自己执行 → 死锁。
三、DispatchGroup 实战:并行任务同步
场景:
有三个网络请求,需要并发执行,全部完成后刷新 UI。
示例:
let group = DispatchGroup()
let queue = DispatchQueue.global()
group.enter()
queue.async {
print("任务1完成")
group.leave()
}
group.enter()
queue.async {
print("任务2完成")
group.leave()
}
group.enter()
queue.async {
print("任务3完成")
group.leave()
}
group.notify(queue: .main) {
print("全部完成,更新UI")
}输出:
任务1完成
任务3完成
任务2完成
全部完成,更新UI说明任务是并发执行的。
四、信号量(Semaphore)实现顺序执行
场景:
多个异步任务需要按顺序执行。
示例:
let semaphore = DispatchSemaphore(value: 0)
let queue = DispatchQueue.global()
queue.async {
print("任务1开始")
sleep(1)
print("任务1结束")
semaphore.signal()
}
semaphore.wait()
queue.async {
print("任务2开始")
sleep(1)
print("任务2结束")
semaphore.signal()
}
semaphore.wait()
print("全部结束")输出:
任务1开始
任务1结束
任务2开始
任务2结束
全部结束信号量通过“等待-释放”机制,实现顺序执行。
五、OperationQueue 实战题:依赖管理
题目:
三个任务:A、B、C。要求 A → B → C 顺序执行。
解法:
let queue = OperationQueue()
let opA = BlockOperation { print("A") }
let opB = BlockOperation { print("B") }
let opC = BlockOperation { print("C") }
opB.addDependency(opA)
opC.addDependency(opB)
queue.addOperations([opA, opB, opC], waitUntilFinished: false)输出:
A
B
C即使在并发队列中,也能通过依赖保证顺序。
六、常见死锁模式总结
| 场景 | 示例 | 原因 |
|---|---|---|
| 主队列中使用 sync | DispatchQueue.main.sync {} | 等待自身执行 |
| 串行队列中嵌套 sync | queue.sync 内再 queue.sync | 自我等待 |
| 锁嵌套 | 两线程互相等待 | 互相持锁 |
| 信号量错误使用 | wait() 未配对 signal() | 永久阻塞 |
七、线程安全题目分析
题目:
var count = 0
let queue = DispatchQueue(label: "com.test", attributes: .concurrent)
for _ in 0..<10_000 {
queue.async {
count += 1
}
}问:结果可能是多少?
答:不确定。
多线程并发修改 count 会发生数据竞争(race condition),导致最终结果小于 10000。
解决方案:
方式一:使用串行队列
let queue = DispatchQueue(label: "com.test.serial")方式二:使用信号量
let semaphore = DispatchSemaphore(value: 1)
queue.async {
semaphore.wait()
count += 1
semaphore.signal()
}方式三:使用 Actor(Swift 并发方式)
actor Counter {
var count = 0
func increment() { count += 1 }
}
let counter = Counter()
Task {
await counter.increment()
}八、Async/Await 面试实战题
题目:
如何让多个异步请求并发执行后汇总结果?
示例:
func loadUser() async -> String { "User" }
func loadPosts() async -> String { "Posts" }
func loadData() async {
async let user = loadUser()
async let posts = loadPosts()
let (u, p) = await (user, posts)
print("结果:\(u) + \(p)")
}输出:
结果:User + Posts面试思路:
- async let 会立即启动异步任务。
- await 等待所有任务完成。
- 不同于串行执行,能充分利用并发。
九、经典考点:主线程异步与同步区别
题目:
DispatchQueue.main.async {
print("A")
}
print("B")输出顺序:B → A(异步,不阻塞主线程)
DispatchQueue.main.sync {
print("A")
}
print("B")会造成死锁(主线程等待自身执行)。
十、QoS(Quality of Service)优化实战
QoS 决定任务优先级与调度策略。
示例:
DispatchQueue.global(qos: .userInteractive).async {
print("用户交互任务")
}
DispatchQueue.global(qos: .background).async {
print("后台任务")
}系统优先执行 .userInteractive 队列任务。
QoS 常见级别:
| 级别 | 用途 | 示例 |
|---|---|---|
| .userInteractive | 即时响应用户操作 | UI 动画、滑动 |
| .userInitiated | 用户主动触发操作 | 加载数据 |
| .utility | 耗时操作 | 下载文件 |
| .background | 后台维护任务 | 日志上传 |
十一、面试常见开放题思路
Q1:如何保证线程安全?
- 使用串行队列、锁、信号量、Actor。
- 保证同一资源在同一时间只被一个线程修改。
Q2:多线程一定更快吗?
不一定。
线程切换开销高,轻量任务反而在单线程中更高效。
Q3:GCD 的内存管理策略?
任务在提交到队列时自动捕获上下文,系统管理线程池,无需手动释放。
Q4:如何避免竞争条件?
通过同步机制(串行化、锁、信号量)确保访问顺序。
Q5:async/await 与 Combine 的区别?
async/await 是语言层的语法糖,适合点对点异步逻辑。
Combine 是响应式框架,适合连续流式数据。
十二、总结
Swift 多线程体系经历了三个阶段演进:
- GCD → 轻量、底层、灵活。
- OperationQueue → 面向对象、支持依赖。
- Swift Concurrency → 安全、直观、结构化并发。
理解这三层的异同,是 iOS 面试中必考的核心知识。
十五、iOS 多线程优化与性能调度策略
一、为什么多线程反而会“变慢”?
多线程不是“越多越好”。
线程的创建、销毁和切换都需要系统资源。
当线程数过多时,会产生:
- 上下文切换开销:CPU 不断保存、恢复寄存器状态。
- 锁竞争:多个线程等待资源释放。
- 缓存失效:不同核心间数据同步开销大。
优化原则:
多线程的目标是“最大化 CPU 有效工作时间”,而不是“最大化线程数量”。
二、主线程优化策略
主线程必须保持轻量,专注于 UI 和交互。
在 iOS 中,主线程阻塞超过 100ms 就可能导致明显卡顿。
常见优化点:
- 避免在主线程执行耗时操作
- 网络请求、JSON 解析、图片压缩、数据库操作必须放在后台线程。
- UI 更新集中处理
- 合并多次 UI 修改成一次统一刷新。
- 使用 Instruments 检测主线程卡顿
- Time Profiler、Main Thread Checker。
三、GCD 性能优化技巧
1. 使用合适的队列类型
// 并行队列适合CPU密集任务
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)
// 串行队列用于数据安全访问
let serialQueue = DispatchQueue(label: "com.app.serial")2. 减少线程创建
重复使用系统提供的全局队列:
DispatchQueue.global(qos: .userInitiated).async { ... }3. 使用
DispatchWorkItem
控制任务
let workItem = DispatchWorkItem {
print("执行任务")
}
DispatchQueue.global().async(execute: workItem)
workItem.cancel() // 可取消任务4. 批处理任务(DispatchGroup)
在多个任务之间实现并行 + 汇总处理,避免串行阻塞。
四、OperationQueue 调优策略
1. 限制并发数
queue.maxConcurrentOperationCount = 3防止过多任务同时启动占满 CPU。
2. 利用依赖关系
通过 addDependency 建立清晰的执行链,减少同步等待。
3. 自定义 Operation
重写 main() 或使用 isCancelled 检查取消状态,增强控制力。
五、Swift Concurrency 调度优化
Swift 并发(async/await、Task、Actor)已经对线程调度进行了优化,但仍需遵守性能原则。
1. 避免过度嵌套 Task
// ❌ 不推荐
Task {
Task {
Task { ... }
}
}每个 Task 都会产生调度开销,应尽量使用结构化任务。
2. 使用
Task.detached
谨慎
Task.detached {
await doSomething()
}Detached Task 不继承父 Task 的上下文,适合真正独立的后台任务,但不宜滥用。
3. 善用
@MainActor
确保 UI 更新在主线程执行,减少锁竞争。
@MainActor func refreshUI() { ... }六、线程安全与数据共享优化
1. 使用串行队列替代锁
let safeQueue = DispatchQueue(label: "com.app.safe")
safeQueue.async {
// 安全访问共享资源
}相比传统锁(NSLock),串行队列开销更小、可读性更高。
2. 使用读写锁(Dispatch Barrier)
适用于“多读少写”场景:
let queue = DispatchQueue(label: "rw.queue", attributes: .concurrent)
func read() {
queue.async { print("读操作") }
}
func write() {
queue.async(flags: .barrier) { print("写操作") }
}3. 使用 Actor 替代锁(Swift Concurrency)
actor DataManager {
private var data = [String]()
func add(_ value: String) { data.append(value) }
}Actor 自动隔离状态,线程安全且高效。
七、常见高性能场景优化案例
1. 图片加载与解码
- 使用异步队列在后台解码图片,避免主线程阻塞。
- 使用缓存(如 NSCache)减少重复解码。
2. 大量 JSON 解析
- 将解析任务放入后台队列。
- 使用 JSONDecoder 的流式解析避免内存峰值。
3. 滑动列表卡顿
- 在后台计算布局(如高度、图片尺寸)。
- 在主线程仅进行 UI 渲染。
4. 网络任务批量执行
利用 DispatchGroup 或 TaskGroup 并行请求,减少总耗时。
八、QoS(服务质量等级)调度策略
GCD 和 Swift Concurrency 都支持 QoS,用于动态调度优先级。
| QoS 等级 | 优先级 | 场景 |
|---|---|---|
| .userInteractive | 最高 | UI 动画、交互操作 |
| .userInitiated | 高 | 用户主动请求 |
| .utility | 中 | 下载、后台计算 |
| .background | 低 | 后台同步、日志上传 |
示例:
DispatchQueue.global(qos: .background).async {
saveLogs()
}系统会根据设备负载自动调整调度频率和线程优先级。
九、异步性能调试工具
1.
Instruments – Time Profiler
分析 CPU 占用与线程调用栈,定位性能瓶颈。
2.
Thread Sanitizer
在 Xcode Scheme 设置中开启,检测数据竞争与死锁风险。
3.
Main Thread Checker
自动检测 UI 操作是否在主线程中执行。
4.
os_signpost / Instruments
自定义打点,分析任务执行时间:
import os.signpost
let log = OSLog(subsystem: "com.app", category: "performance")
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "HeavyTask", signpostID: signpostID)
// 执行任务
os_signpost(.end, log: log, name: "HeavyTask", signpostID: signpostID)十、最佳实践总结
-
主线程只负责 UI 与事件
任何耗时逻辑都必须异步执行。
-
控制并发量
并发 ≠ 快速。避免线程暴增。
-
数据访问串行化
保证一致性优先于速度。
-
任务解耦与依赖管理
使用 OperationQueue 或 TaskGroup 管理复杂任务关系。
-
合理分配 QoS
让系统理解任务的重要程度,调度更智能。
-
主动检测卡顿与竞争
使用 Instruments 工具监控性能。
十一、面试开放题延伸方向
Q1:如何在 Swift Concurrency 中实现任务取消?
使用 Task.isCancelled 或 Task.checkCancellation() 手动检测。
for i in 1...10 {
if Task.isCancelled { break }
print(i)
}Q2:如何避免“主线程阻塞”?
- 后台线程执行耗时任务。
- UI 操作封装为异步更新。
- 分块加载大数据。
Q3:在多核设备中如何最大化性能?
- 利用并行队列让任务在多个核心同时执行。
- 避免频繁切换上下文。
- 通过 QoS 平衡任务优先级。
十二、总结
多线程的真正考验不在于语法,而在于设计哲学:
在保证线程安全的前提下,让 CPU 处于“最合理的忙碌状态”。
Swift 从 GCD 到 OperationQueue,再到 async/await,是从控制线程到控制逻辑流的进化。
理解这一脉络,能让你在面试和实际项目中更从容地应对各种并发问题。