iOS 开发中的多线程

由浅入深,从基本概念到源码解析,带你全面理解 iOS 并发编程


一、为什么需要多线程?

1.1 单线程的局限

在移动应用中,主线程(Main Thread/UI Thread) 负责:

  • 处理用户交互(点击、滑动等)
  • 更新 UI
  • 处理 RunLoop 事件

如果耗时操作(网络请求、大文件读写、复杂计算)在主线程执行,会导致:

  • 界面卡顿:主线程被阻塞,无法及时响应触摸
  • ANR(Application Not Responding):系统可能强制终止「无响应」的 App
  • 糟糕的用户体验
1
2
3
4
5
6
// ❌ 错误示例:主线程执行网络请求
func loadData() {
let url = URL(string: "https://api.example.com/data")!
let data = try? Data(contentsOf: url) // 阻塞主线程!
self.tableView.reloadData()
}

1.2 多线程的核心思想

将耗时任务放到子线程执行,完成后回到主线程更新 UI:

1
2
3
主线程:响应用户 → 派发任务到子线程 → 继续处理 UI
子线程:执行耗时任务 → 完成 → 通知主线程
主线程:收到结果 → 更新 UI

二、iOS 多线程技术栈

2.1 技术对比

技术 抽象层次 使用场景 学习曲线
Thread 底层,直接操作线程 需要精细控制线程生命周期
GCD 任务队列,无需管理线程 绝大多数异步任务
Operation 面向对象,可取消/依赖/优先级 复杂任务编排 中低

2.2 选择建议

  • 首选 GCD:简单异步、串行/并发队列、延迟执行、一次性执行
  • Operation:需要取消、依赖关系、暂停恢复、进度回调
  • Thread:极少需要,仅当必须直接操作线程时使用

三、GCD(Grand Central Dispatch)详解

3.1 核心概念

GCD 是苹果提供的并发编程框架,基于 C 库 libdispatch。它采用「任务 + 队列」模型:

  • 任务(Block/Closure):要执行的代码块
  • 队列(Queue):存放任务,按规则调度到线程执行
1
2
3
4
5
6
7
8
9
// 基本用法
DispatchQueue.global().async {
// 子线程执行
let result = doHeavyWork()
DispatchQueue.main.async {
// 主线程更新 UI
self.updateUI(result)
}
}

3.2 队列类型

队列 类型 说明
主队列 串行 DispatchQueue.main,主线程执行,用于 UI 更新
全局队列 并发 DispatchQueue.global(qos:),系统管理线程池
自定义串行队列 串行 同一时刻只执行一个任务
自定义并发队列 并发 可同时执行多个任务
1
2
3
4
5
6
7
8
9
// 串行队列:任务按顺序执行
let serialQueue = DispatchQueue(label: "com.app.serial")

// 并发队列:任务可并发执行
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)

// QoS(服务质量)优先级
DispatchQueue.global(qos: .userInitiated).async { } // 用户发起,需快速响应
DispatchQueue.global(qos: .background).async { } // 后台任务,可慢速

3.3 QoS 优先级

QoS 说明 典型场景
.userInteractive 用户交互,最高优先级 动画、即时反馈
.userInitiated 用户发起,高优先级 加载数据、点击后处理
.default 默认 无特别需求
.utility 实用型 下载、导入
.background 后台 同步、预加载

3.4 常用 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 异步执行
queue.async { }

// 同步执行(会阻塞当前线程直到任务完成)
queue.sync { }

// 延迟执行
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { }

// 只执行一次(如单例)
var token: Int = 0
DispatchQueue.once(&token) {
// 全局只执行一次
}

// DispatchWorkItem:可取消的任务
let workItem = DispatchWorkItem { print("working") }
queue.async(execute: workItem)
workItem.cancel() // 取消(若已开始则无法取消)

3.5 dispatch_barrier:读写锁场景

在自定义并发队列中,barrier 可保证「屏障前的任务完成后,再执行屏障任务,屏障完成后再执行屏障后的任务」:

1
2
3
4
5
6
7
8
9
let concurrentQueue = DispatchQueue(label: "com.app.db", attributes: .concurrent)

// 读:可并发
concurrentQueue.async { self.readFromCache() }

// 写:barrier 保证独占
concurrentQueue.async(flags: .barrier) {
self.writeToCache(newValue)
}

四、NSOperation 与 NSOperationQueue

4.1 为什么需要 Operation?

GCD 虽然强大,但在以下场景不够灵活:

  • 需要取消尚未执行的任务
  • 需要任务依赖(A 完成后再执行 B)
  • 需要暂停/恢复队列
  • 需要进度完成回调

Operation 提供了面向对象的方式解决这些问题。

4.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
// 使用 BlockOperation
let op = BlockOperation {
print("执行任务")
}
op.completionBlock = { print("任务完成") }

let queue = OperationQueue()
queue.addOperation(op)

// 设置最大并发数
queue.maxConcurrentOperationCount = 3

4.3 任务依赖

1
2
3
4
5
6
7
8
let opA = BlockOperation { downloadImage() }
let opB = BlockOperation { resizeImage() }
let opC = BlockOperation { uploadImage() }

opB.addDependency(opA) // B 依赖 A
opC.addDependency(opB) // C 依赖 B

queue.addOperations([opA, opB, opC], waitUntilFinished: false)

4.4 自定义 Operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ImageLoadOperation: Operation {
let url: URL
var image: UIImage?

init(url: URL) {
self.url = url
super.init()
}

override var isAsynchronous: Bool { true }

override func main() {
guard !isCancelled else { return }
if let data = try? Data(contentsOf: url) {
image = UIImage(data: data)
}
}
}

4.5 取消与暂停

1
2
3
queue.cancelAllOperations()   // 取消所有未执行任务
queue.isSuspended = true // 暂停队列(不执行新任务)
queue.isSuspended = false // 恢复

五、线程同步与线程安全

5.1 为什么需要同步?

多个线程同时访问共享资源(变量、文件、网络连接)时,若未做同步,会出现:

  • 数据竞争:读写交错,结果不可预期
  • 脏读:读到未写入完成的数据
  • 崩溃:如数组在遍历时被另一线程修改
1
2
3
4
5
6
// ❌ 非线程安全
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter += 1 // 多线程同时写,结果可能远小于 1000
}
print(counter) // 可能输出 523、687 等

5.2 锁机制

5.2.1 NSLock

1
2
3
4
5
6
7
8
9
let lock = NSLock()
var counter = 0

DispatchQueue.concurrentPerform(iterations: 1000) { _ in
lock.lock()
defer { lock.unlock() }
counter += 1
}
print(counter) // 1000

5.2.2 os_unfair_lock(高性能,iOS 10+)

1
2
3
4
var unfairLock = os_unfair_lock()
os_unfair_lock_lock(&unfairLock)
// 临界区
os_unfair_lock_unlock(&unfairLock)

5.2.3 NSRecursiveLock(可重入锁)

同一线程可多次加锁,用于递归或嵌套调用:

1
2
3
4
5
6
7
8
let recursiveLock = NSRecursiveLock()
func recursiveMethod(_ n: Int) {
recursiveLock.lock()
defer { recursiveLock.unlock() }
if n > 0 {
recursiveMethod(n - 1)
}
}

5.2.4 @synchronized(Objective-C)

1
2
3
@synchronized(self) {
// 临界区
}

底层使用 objc_sync_enter / objc_sync_exit,基于对象做锁。

5.3 信号量(Semaphore)

控制并发数量或实现生产者-消费者

1
2
3
4
5
6
7
8
9
let semaphore = DispatchSemaphore(value: 3)  // 最多 3 个并发

for i in 0..<10 {
DispatchQueue.global().async {
semaphore.wait() // 资源 -1,若为 0 则等待
defer { semaphore.signal() } // 资源 +1
doWork(i)
}
}

5.4 原子操作(Atomic)

对于简单类型,可使用原子属性。Swift 中常用 NSLock + 属性封装,或使用 objc_setAssociatedObject 的原子选项。atomic 属性只保证 getter/setter 原子,不保证复合操作(如 count++)的原子性。

5.5 避免死锁

死锁:两个或多个线程互相等待对方释放资源。

1
2
3
4
// ❌ 死锁示例:主队列同步执行
DispatchQueue.main.sync {
print("永远不会执行") // 主线程等待自己,死锁
}
1
2
3
4
5
6
7
// ❌ 死锁:串行队列嵌套同步
let queue = DispatchQueue(label: "serial")
queue.async {
queue.sync {
print("死锁") // 外层等内层,内层等外层
}
}

原则:避免在同一串行队列中嵌套 sync 调用。


六、Swift 并发(async/await)

6.1 从回调到 async/await

传统异步代码容易产生「回调地狱」:

1
2
3
4
5
6
7
8
// 回调嵌套
loadUser(id: 1) { user in
loadPosts(userId: user.id) { posts in
loadComments(postId: posts[0].id) { comments in
// 层层嵌套...
}
}
}

Swift 5.5 引入 async/await,写法更清晰:

1
2
3
4
5
6
func loadUserData() async throws {
let user = try await loadUser(id: 1)
let posts = try await loadPosts(userId: user.id)
let comments = try await loadComments(postId: posts[0].id)
await MainActor.run { self.updateUI(comments) }
}

6.2 Task 与 MainActor

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建异步任务
Task {
let result = await fetchData()
await MainActor.run {
self.label.text = result
}
}

// MainActor:保证在主线程执行
@MainActor
class ViewController: UIViewController {
func updateUI() { } // 自动在主线程
}

6.3 与 GCD 的桥接

1
2
3
4
5
6
7
8
9
// GCD 转 async
func withCheckedContinuation() async {
await withCheckedContinuation { continuation in
DispatchQueue.global().async {
let result = doWork()
continuation.resume(returning: result)
}
}
}

七、RunLoop 与线程

每个线程都有唯一的 RunLoop。子线程默认不启动 RunLoop,若使用 performSelector:onThread:NSTimer,需要手动 run

1
2
3
4
5
6
class WorkerThread: Thread {
override func main() {
RunLoop.current.add(Port(), forMode: .default)
RunLoop.current.run() // 进入事件循环
}
}

主线程的 RunLoop 由系统自动运行,通常无需关心。


八、GCD 底层原理(libdispatch 简析)

8.1 队列与线程池

  • GCD 维护全局线程池,根据队列类型和系统负载动态创建/复用线程
  • 串行队列:通常绑定一个线程,任务在该线程顺序执行
  • 并发队列:任务可分发到线程池中的多个线程

8.2 任务提交流程(简化)

1
2
3
4
5
6
7
dispatch_async(queue, block)

├─ 将 block 封装为 dispatch_continuation_t

├─ 将任务加入队列的 FIFO 链表

└─ 若有空闲 worker 线程,则唤醒执行;否则根据需要创建新线程

8.3 主队列特殊性

主队列任务一定在主线程执行,通过 RunLoop 的 Source1(Mach Port)唤醒主线程处理。

8.4 源码参考

libdispatch 开源:https://github.com/apple/swift-corelibs-libdispatch

核心结构体(简化):

1
2
3
4
5
6
7
8
9
struct dispatch_queue_s {
// 队列类型:串行 / 并发
// 目标队列、任务链表等
};

struct dispatch_continuation_s {
void (*dc_func)(void *); // block 的执行函数
void *dc_ctxt; // block 捕获的上下文
};

九、实战示例

9.1 图片异步加载与缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ImageLoader {
private let cache = NSCache<NSString, UIImage>()
private let queue = DispatchQueue(label: "com.app.imageloader", attributes: .concurrent)

func loadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
DispatchQueue.main.async { completion(cached) }
return
}
queue.async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
DispatchQueue.main.async { completion(nil) }
return
}
self.cache.setObject(image, forKey: key)
DispatchQueue.main.async { completion(image) }
}
}
}

9.2 多接口并发请求,统一回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func loadDashboardData(completion: @escaping (DashboardData?) -> Void) {
let group = DispatchGroup()
var user: User?
var orders: [Order]?
var error: Error?

group.enter()
fetchUser { result in
user = try? result.get()
group.leave()
}

group.enter()
fetchOrders { result in
orders = (try? result.get()) ?? []
group.leave()
}

group.notify(queue: .main) {
if let u = user, let o = orders {
completion(DashboardData(user: u, orders: o))
} else {
completion(nil)
}
}
}

9.3 线程安全的单例

1
2
3
4
5
6
7
8
final class DataManager {
static let shared: DataManager = {
let instance = DataManager()
return instance
}()

private init() { }
}

若需「懒加载 + 线程安全」,可使用 dispatch_once 的 Swift 封装,或在 Swift 中依赖 static let 的天然懒加载与线程安全。


十、实际项目中的应用案例

10.1 列表图片预加载

UITableView / UICollectionView 滚动时,预加载即将出现的 cell 所需图片,放到后台队列解码,再回主线程赋值,避免主线程卡顿。

1
2
3
4
5
6
7
8
9
10
11
func prefetchImage(at indexPath: IndexPath) {
let url = urls[indexPath.row]
DispatchQueue.global(qos: .utility).async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.imageCache.setObject(image, forKey: url.absoluteString as NSString)
self.collectionView.reloadItems(at: [indexPath])
}
}
}

10.2 大文件分片上传

将大文件切分为多个 chunk,通过 OperationQueue 控制并发数,每个 Operation 上传一个 chunk,全部完成后组装结果。

1
2
3
4
5
6
7
8
let uploadQueue = OperationQueue()
uploadQueue.maxConcurrentOperationCount = 3
for chunk in chunks {
let op = BlockOperation { uploadChunk(chunk) }
uploadQueue.addOperation(op)
}
uploadQueue.waitUntilAllOperationsAreFinished()
mergeChunks()

10.3 搜索防抖

用户输入时,取消上一次未完成的搜索任务,延迟 300ms 再发起新请求:

1
2
3
4
5
6
7
8
9
var searchWorkItem: DispatchWorkItem?
func searchTextDidChange(_ text: String) {
searchWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.performSearch(text)
}
searchWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}

10.4 后台数据同步

App 进入后台时,使用 beginBackgroundTask 申请有限时间,在后台队列执行同步逻辑,完成后结束 task:

1
2
3
4
5
6
7
8
9
10
11
12
func syncInBackground() {
var taskID: UIBackgroundTaskIdentifier = .invalid
taskID = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(taskID)
}
DispatchQueue.global(qos: .utility).async {
performSync()
DispatchQueue.main.async {
UIApplication.shared.endBackgroundTask(taskID)
}
}
}

十一、常见问题与最佳实践

11.1 主线程检查

开发阶段可用断言检查 UI 是否在主线程更新:

1
assert(Thread.isMainThread, "UI must be updated on main thread")

11.2 避免循环引用

在闭包中使用 self 时注意 [weak self]

1
2
3
4
5
6
7
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
let result = self.doWork()
DispatchQueue.main.async { [weak self] in
self?.updateUI(result)
}
}

11.3 合理选择 QoS

不要滥用 .userInteractive,避免后台任务抢占用户交互资源。

11.4 优先使用 Swift 并发

新项目优先考虑 async/await + Task,结构更清晰,可组合性更好。


十二、总结

场景 推荐方案
简单异步、延迟执行 GCD
复杂任务依赖、取消、暂停 NSOperation
新项目、网络/IO 密集型 async/await
控制并发数 信号量 或 OperationQueue.maxConcurrentOperationCount
读写分离、缓存 dispatch_barrier
线程安全访问共享资源 NSLock / os_unfair_lock

多线程能提升体验,但也会带来复杂度和潜在问题。理解原理、选对工具、注意同步与线程安全,是写出高质量并发代码的关键。