4. 多线程与并发编程:GCD、NSOperation、锁机制

iOS 并发编程是面试高频区,考察候选人对多线程工具的理解深度、实际场景选型能力以及锁机制的正确使用。本章聚焦三大核心工具:GCD(串行/并行队列、Dispatch Group、Semaphore)、NSOperation(依赖、优先级、取消)以及锁机制(@synchronized、NSLock、dispatch_semaphore、os_unfair_lock)。

学习目标:
  • 掌握 GCD 与 NSOperation 的选型原则(以下为选型对照表)
  • 理解常见死锁场景及避免方法
  • 能结合项目经验讲解锁的性能对比

工具选型对照表

维度GCDNSOperation
抽象层级C 语言 API,轻量OC 对象,更重
依赖管理需手动 barrier 或 Group原生 addDependency
取消操作不支持(需自建标志位)支持 cancel
最大并发数控制dispatch_semaphore 或自定义队列maxConcurrentOperationCount
推荐场景简单异步、轻量并发任务复杂依赖、可取消/可暂停任务
多线程问题回答框架 1. 明确场景 同步/异步?串行/并行? 2. 选择工具 GCD / NSOperation / Lock 3. 正确使用 避免死锁、资源竞争 4. 性能与安全 锁粒度、优先级反转 面试中按此框架展开,展示结构化思维 加分点:主动对比 OSSpinLock vs os_unfair_lock

面试问答卡片

卡片1:GCD 串行队列与并发队列的区别及实际选型

面试问题:请解释 GCD 串行队列和并发队列的区别。在实际开发中你如何选择?

面试官考察点:基础概念清晰度;能否结合场景说明(如同步资源访问 vs 并发下载)。

回答框架:

  1. 定义:串行队列一次只执行一个任务;并发队列同时执行多个任务(但受系统线程池管理)。
  2. 选型:需要保护共享资源 → 串行队列(或 dispatch barrier);需要提升 IO/网络并发 → 并发队列。
  3. 示例:用并发队列做图片批量下载,用串行队列做数据库写入。

简短示例回答:串行队列保证任务按顺序执行,适合写操作;并发队列可同时跑多个任务,适合读多写少或独立任务。比如我自己实现的图片缓存库,读写缓存用串行队列加 barrier 保证线程安全,加载网络用并发队列提升速度。

可能追问:① 如何在并发队列中实现“读写锁”的效果?② 串行队列内嵌套同步派发会导致什么?③ 主队列属于哪种队列?

避坑提醒:别只说“串行就是一次一个”而忽略 dispatch_get_main_queue 的特殊性。回答选型时务必举例,否则显得空洞。

卡片2:NSOperation 依赖管理与取消操作

面试问题:NSOperationQueue 如何实现操作依赖?能否取消正在执行的操作?

面试官考察点:是否理解 NSOperation 对比 GCD 的优势;是否知道取消的局限性(需配合 isCancelled 检查)。

回答框架:

  1. 依赖管理:operationB.addDependency(operationA),需等 A 完成才执行 B。
  2. 取消:调用 queue.cancelAllOperations 或 operation.cancel(),但已开始的任务不会自动停止,需在 NSOperation 的 main 方法内定期检查 self.isCancelled。
  3. 适用场景:上传多个文件需依次依赖、任务可中断(如下载暂停)。

简短示例回答:在 App 启动预加载需求中,我使用 NSOperationQueue 管理依赖:先下载配置 JSON,再根据 JSON 加载图片和文字,通过 addDependency 控制顺序;用户退出时调用 cancelAllOperations 并检查 isCancelled 及时中断资源消耗。

可能追问:① NSOperation 的 isReady、isFinished 状态如何维护?② GCD 能否方便地取消操作?③ 怎样避免取消后资源泄漏?

避坑提醒:不要只说“NSOperationQueue 可以取消”,必须补充“取消后仍需在 main 里主动检测”。忘记 isCancelled 是常见扣分项。

卡片3:锁机制选择与性能对比

面试问题:iOS 开发中常用的锁有哪些?你如何选择?

面试官考察点:对锁的性能、使用限制的理解;能否说出最新的推荐(os_unfair_lock 替换 OSSpinLock)。

回答框架:

  1. 列举:@synchronized、NSLock、dispatch_semaphore、pthread_mutex、os_unfair_lock。
  2. 性能排序:os_unfair_lock / dispatch_semaphore ≈ pthread_mutex > NSLock > @synchronized。
  3. 选型原则:简单场景用 @synchronized;高性能场景用 os_unfair_lock 或 dispatch_semaphore;多读单写用 pthread_rwlock。
  4. 注意:OSSpinLock 已废弃(优先级反转问题)。

简短示例回答:我在缓存组件中采用 dispatch_semaphore 做读写互斥;对于高频强占锁的场景(如统计计数)改用 os_unfair_lock。对比测试后性能提升约 30%,且避免了优先级反转。

可能追问:① @synchronized 的底层原理?② 什么是死锁?举一个 GCD 死锁的例子?③ 如何用信号量实现最大并发数控制?

避坑提醒:谨记 OSSpinLock 已被弃用,回答时最好主动指出。不要只背锁名字,要结合场景说明优缺点。
锁机制追问路径 性能对比 最快 vs 最慢 死锁场景 GCD 主队列同步 信号量应用 控制并发数 底层原理 @synchronized 实现 面试官会顺着这些分支深挖,提前准备好每一条的示例 如:主队列同步 → 死锁;@synchronized → 关联对象 + 递归锁

项目案例:多线程优化实战

项目名称:高并发图片缓存与预加载模块

业务背景:社交 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。” 这段话术可套用到其他需要并发安全 + 可取消的模块(如日志上传、数据同步)。

面试加分点:提及使用 Instrument 的 Thread Sanitizer 检测数据竞争,以及使用 os_unfair_lock 优化高频自增统计。