4. 多线程与并发编程:GCD、NSOperation、锁机制
iOS 并发编程是面试高频区,考察候选人对多线程工具的理解深度、实际场景选型能力以及锁机制的正确使用。本章聚焦三大核心工具:GCD(串行/并行队列、Dispatch Group、Semaphore)、NSOperation(依赖、优先级、取消)以及锁机制(@synchronized、NSLock、dispatch_semaphore、os_unfair_lock)。
- 掌握 GCD 与 NSOperation 的选型原则(以下为选型对照表)
- 理解常见死锁场景及避免方法
- 能结合项目经验讲解锁的性能对比
工具选型对照表
| 维度 | GCD | NSOperation |
|---|---|---|
| 抽象层级 | C 语言 API,轻量 | OC 对象,更重 |
| 依赖管理 | 需手动 barrier 或 Group | 原生 addDependency |
| 取消操作 | 不支持(需自建标志位) | 支持 cancel |
| 最大并发数控制 | dispatch_semaphore 或自定义队列 | maxConcurrentOperationCount |
| 推荐场景 | 简单异步、轻量并发任务 | 复杂依赖、可取消/可暂停任务 |
面试问答卡片
卡片1:GCD 串行队列与并发队列的区别及实际选型
面试问题:请解释 GCD 串行队列和并发队列的区别。在实际开发中你如何选择?
面试官考察点:基础概念清晰度;能否结合场景说明(如同步资源访问 vs 并发下载)。
回答框架:
- 定义:串行队列一次只执行一个任务;并发队列同时执行多个任务(但受系统线程池管理)。
- 选型:需要保护共享资源 → 串行队列(或 dispatch barrier);需要提升 IO/网络并发 → 并发队列。
- 示例:用并发队列做图片批量下载,用串行队列做数据库写入。
简短示例回答:串行队列保证任务按顺序执行,适合写操作;并发队列可同时跑多个任务,适合读多写少或独立任务。比如我自己实现的图片缓存库,读写缓存用串行队列加 barrier 保证线程安全,加载网络用并发队列提升速度。
可能追问:① 如何在并发队列中实现“读写锁”的效果?② 串行队列内嵌套同步派发会导致什么?③ 主队列属于哪种队列?
卡片2:NSOperation 依赖管理与取消操作
面试问题:NSOperationQueue 如何实现操作依赖?能否取消正在执行的操作?
面试官考察点:是否理解 NSOperation 对比 GCD 的优势;是否知道取消的局限性(需配合 isCancelled 检查)。
回答框架:
- 依赖管理:operationB.addDependency(operationA),需等 A 完成才执行 B。
- 取消:调用 queue.cancelAllOperations 或 operation.cancel(),但已开始的任务不会自动停止,需在 NSOperation 的 main 方法内定期检查 self.isCancelled。
- 适用场景:上传多个文件需依次依赖、任务可中断(如下载暂停)。
简短示例回答:在 App 启动预加载需求中,我使用 NSOperationQueue 管理依赖:先下载配置 JSON,再根据 JSON 加载图片和文字,通过 addDependency 控制顺序;用户退出时调用 cancelAllOperations 并检查 isCancelled 及时中断资源消耗。
可能追问:① NSOperation 的 isReady、isFinished 状态如何维护?② GCD 能否方便地取消操作?③ 怎样避免取消后资源泄漏?
卡片3:锁机制选择与性能对比
面试问题:iOS 开发中常用的锁有哪些?你如何选择?
面试官考察点:对锁的性能、使用限制的理解;能否说出最新的推荐(os_unfair_lock 替换 OSSpinLock)。
回答框架:
- 列举:@synchronized、NSLock、dispatch_semaphore、pthread_mutex、os_unfair_lock。
- 性能排序:os_unfair_lock / dispatch_semaphore ≈ pthread_mutex > NSLock > @synchronized。
- 选型原则:简单场景用 @synchronized;高性能场景用 os_unfair_lock 或 dispatch_semaphore;多读单写用 pthread_rwlock。
- 注意:OSSpinLock 已废弃(优先级反转问题)。
简短示例回答:我在缓存组件中采用 dispatch_semaphore 做读写互斥;对于高频强占锁的场景(如统计计数)改用 os_unfair_lock。对比测试后性能提升约 30%,且避免了优先级反转。
可能追问:① @synchronized 的底层原理?② 什么是死锁?举一个 GCD 死锁的例子?③ 如何用信号量实现最大并发数控制?
项目案例:多线程优化实战
项目名称:高并发图片缓存与预加载模块
业务背景:社交 App 信息流需要多图加载,低端机滚动时卡顿,原实现未做线程安全控制导致 Crash。
规模指标:日活 200 万;图片缓存容量约 200MB;并发下载峰值 20 个任务。
技术栈:NSOperationQueue + NSOperation(可取消)、dispatch_barrier 实现读写锁、NSCache 结合文件名 Hash。
核心难点:
- 多线程同时读写内存缓存导致 EXC_BAD_ACCESS
- 用户快速滑动产生大量待取消下载任务,内存泄漏
- 不同优先级任务饥饿
面试高频追问:
- 为什么选择 NSOperation 而不是 GCD?——需要取消、依赖、优先级。
- 如何实现图片缓存 LRU 淘汰?——NSCache 自带缓存淘汰及节省系统内存机制。
- 怎样防止图片下载任务重复?——下载前先检查是否已在队列中(用 NSMutableSet 记录 URL+大小)。
可迁移回答话术:“在图片预加载项目中,我采用 NSOperationQueue 管理并发任务,利用依赖控制批次下载,并实现取消机制。同时通过 dispatch_barrier 确保内存缓存的读写安全,最终滑动帧率从 45fps 提升至 58fps。” 这段话术可套用到其他需要并发安全 + 可取消的模块(如日志上传、数据同步)。