线程并发与 RunLoop 面试题库
范围说明
本文只覆盖 iOS 面试里与线程、并发模型、同步原语、RunLoop 和 Swift Concurrency 相关的高频问题,不展开 UI 渲染细节和网络协议细节。
适用对象:iOS 初级、中级、高级面试。阅读目标是“能快速背诵,也能追问时讲透”。
高频问题清单
1. pthread 和 NSThread 的定位是什么?
问题: pthread、NSThread、GCD、NSOperation 分别解决什么问题?为什么面试总会先问线程?
考点: 线程抽象层级、系统线程模型、开发者关注点从“线程”到“任务”的演进。
回答要点: pthread 是 POSIX 线程接口,最底层、最灵活、最难用;NSThread 是对 pthread 的 Cocoa 封装,保留了线程概念但更易用;GCD 和 NSOperation 关注的是“任务调度”,不是直接管理线程。iOS 开发里大多数场景不需要手动创建线程,优先考虑队列和任务抽象。
高频追问: NSThread 能不能复用?线程创建开销大在哪?为什么不直接用 pthread?
易错点: 把 NSThread 说成“比 GCD 更高级的并发方案”;把线程和队列混为一谈。
2. GCD 的核心思想是什么?
问题: GCD 为什么能简化并发编程?它本质上是什么?
考点: 任务、队列、提交、线程池、系统调度。
回答要点: GCD 的核心是把“要做什么”提交到“什么队列”上执行,系统负责线程创建和复用。开发者只需要描述任务和执行顺序,不需要显式管理线程生命周期。GCD 的价值在于减少线程管理复杂度,提高复用和调度效率。
高频追问: GCD 和线程池是什么关系?GCD 里的线程数量谁决定?
易错点: 认为“一个队列对应一个线程”;认为“GCD 就是多线程”。
3. 串行队列、并发队列、同步和异步怎么理解?
问题:serial queue、concurrent queue、sync、async 的组合结果是什么?
考点: 队列执行语义、是否阻塞当前线程、任务并发度。
回答要点: 串行队列保证任务按提交顺序一个接一个执行;并发队列允许多个任务同时推进,但最终并发度由系统决定。sync 会阻塞当前线程等待任务完成,async 会立即返回。组合时要区分“提交方式”和“队列属性”,不能混着看。
高频追问: 同步一定在当前线程执行吗?并发队列一定并行吗?
易错点: 把“并发”理解成“多线程同时执行”;忽略 sync 会阻塞调用线程。
4. dispatch_barrier_async 是怎么保证互斥的?
问题: barrier 适合什么场景?为什么它常用于读写分离?
考点: 并发队列上的栅栏语义、写入独占、前后任务顺序。
回答要点: barrier 只在自建并发队列上有完整意义。它会等待前面的任务全部完成,再独占执行 barrier 任务,执行完后再放行后续任务。典型用途是多读少写场景:普通读任务并发执行,写任务用 barrier 保证互斥。
高频追问: 为什么不能随便在全局并发队列上用 barrier?
易错点: 把 barrier 当成“万能锁”;在非自建并发队列上误用。
5. dispatch_group 的作用是什么?
问题: group 解决的是“并发执行”还是“结果汇总”问题?
考点: 异步任务编排、通知回调、等待机制。
回答要点: group 的本质是做任务聚合和完成通知。它适合多个异步任务并发执行,全部结束后再统一处理结果。常见 API 是 enter/leave 和 notify,也可以用 wait 但要注意阻塞风险。
高频追问:wait 和 notify 的区别?enter/leave 忘记配对会怎样?
易错点: 用 group.wait() 卡住主线程;enter/leave 计数不平衡导致永远不回调。
6. dispatch_semaphore 的本质是什么?
问题: semaphore 和锁有什么区别?为什么它既能限流又能同步?
考点: 计数信号量、资源许可、线程阻塞与唤醒。
回答要点: semaphore 本质是一个计数器,wait 会尝试获取许可,拿不到就阻塞;signal 会释放许可并唤醒等待者。它可以用来限制并发数,也可以做线程间同步。与锁相比,semaphore 不只用于“互斥”,还常用于“资源池限流”。
高频追问: 用 semaphore 实现互斥锁有什么问题?wait 超时有什么意义?
易错点: 把 semaphore 当成“更通用、更安全的锁”;忘记 signal。
7. 死锁是怎么产生的?
问题: iOS 面试里最常见的死锁场景有哪些?
考点: 自我等待、资源循环依赖、同步调用、主线程阻塞。
回答要点: 死锁的本质是多个执行单元互相等待,且没有任何一方能继续推进。常见场景包括:同一串行队列里同步派发到自己;主线程同步等待主线程任务;多把锁的顺序相反导致循环等待。判断死锁时要先看“是否存在不可打破的等待环”。
高频追问: 为什么 dispatch_sync 到当前串行队列会死锁?
易错点: 只会背结论,不会分析等待链;把“卡住”都叫死锁。
8. 活锁、饥饿和优先级反转分别是什么?
问题: 它们和死锁有什么区别?优先级反转为什么常被问到?
考点: 线程调度公平性、资源竞争、调度优先级。
回答要点: 活锁是线程没有阻塞,但一直在“让步、重试、再让步”,始终无法完成;饥饿是某些线程长期得不到执行机会;优先级反转是低优先级线程持有高优先级线程需要的资源,导致高优先级线程被间接拖慢。它们都属于并发调度问题,但不是死锁。
高频追问: 优先级反转怎么缓解?为什么 mutex 里会提到 priority inheritance?
易错点: 把活锁说成死锁;把“线程没跑起来”都归因于锁。
9. 什么是内存可见性?
问题: 一个线程改了变量,另一个线程为什么不一定立刻看到?
考点: CPU 缓存、编译器重排序、指令重排、同步边界。
回答要点: 内存可见性不是“写了就一定马上对别的线程可见”。线程可能先写到寄存器、缓存或局部顺序里,另一个线程读取到的还是旧值。锁、原子操作、队列切换、栅栏语义等都能建立同步边界,帮助保证读写顺序和可见性。
高频追问:atomic 属性能保证线程安全吗?
易错点: 把“原子性”和“可见性”混为一谈;认为单纯读写 atomic 属性就够了。
10. NSOperation / OperationQueue 比 GCD 强在哪里?
问题: 既然有 GCD,为什么还要用 NSOperationQueue?
考点: 任务对象化、依赖管理、取消、状态、KVO、优先级。
回答要点: Operation 是任务对象,适合封装复杂业务步骤。相比 GCD,它支持依赖关系、取消、暂停/继续、最大并发数、任务优先级、完成状态观察等。GCD 更轻量,OperationQueue 更适合有编排和生命周期管理需求的场景。
高频追问: Operation 的 cancel 是不是立刻停止执行?
易错点: 认为 OperationQueue 只是“GCD 的封装”;误以为 cancel 会强杀线程。
11. os_unfair_lock、NSLock、递归锁、读写锁怎么选?
问题: 常见锁的特点和适用场景分别是什么?
考点: 互斥性能、可重入、读多写少、死锁风险。
回答要点:os_unfair_lock 是更轻量的互斥锁,性能高,但不能递归;NSLock 使用方便,功能通用;递归锁允许同一线程重复加锁,适合递归或层层调用链;读写锁适合读多写少,允许多个读并发、写独占。选锁时先看是否需要可重入,再看是否读写分离,再看性能。
高频追问: 为什么不建议滥用递归锁?读写锁一定比互斥锁快吗?
易错点: 把“能用”当成“该用”;忽略锁竞争下的实际开销。
12. RunLoop 是什么?
问题: RunLoop 在 iOS 里到底负责什么?
考点: 事件循环、输入源、定时器、观察者、线程常驻。
回答要点: RunLoop 是线程上的事件处理循环,负责在没有任务时休眠、在有事件时唤醒并分发。它能处理输入源、定时器、观察者回调,也支持线程长期存活。主线程的 RunLoop 是应用事件驱动的核心之一。
高频追问: 为什么子线程默认没有常驻 RunLoop?
易错点: 把 RunLoop 说成“线程”;把它理解成“只管 UI 的东西”。
13. RunLoop 的 mode 有什么作用?
问题: 为什么要分 default、tracking、common 等模式?
考点: 事件分组、模式隔离、避免互相干扰。
回答要点: mode 是 RunLoop 在某一时刻只处理的一组输入源、定时器和观察者。切换 mode 可以隔离不同场景下的事件处理。commonModes 不是单独模式,而是一组被标记为“常见模式”的集合,方便把 timer 或 source 同时加入多个模式。
高频追问: 为什么滑动时某些 timer 会暂停?
易错点: 把 commonModes 误解成“万能模式”;忘记模式切换会影响事件分发。
14. Timer 和 CADisplayLink 为什么会受 RunLoop 影响?
问题: 定时器为什么有时会延迟、暂停或合并触发?
考点: RunLoop 依附、模式切换、触发时机、主线程负载。
回答要点:Timer 和 CADisplayLink 都依赖 RunLoop 触发。它们是否执行,取决于所在 RunLoop 是否运行、当前 mode 是否匹配、线程是否忙。Timer 更偏时间驱动,CADisplayLink 更偏屏幕刷新节奏驱动,但核心都要经过 RunLoop 分发。
高频追问: 如何让 timer 在滚动时也能继续触发?
易错点: 认为 timer 是独立线程在跑;把延迟都归因于“系统不准”。
15. 事件循环和 RunLoop 的关系是什么?
问题: 主线程为什么说是“事件循环模型”?
考点: 事件接收、分发、处理、睡眠与唤醒。
回答要点: 事件循环就是线程不断从输入源、定时器、观察者中取事件并执行的过程。RunLoop 提供了这个循环框架:没有事件时休眠,有事件时唤醒处理,然后继续等待下一轮事件。应用层很多响应式行为,本质上都依赖这个循环。
高频追问: RunLoop Observer 常见用途是什么?
易错点: 把事件循环只理解成“轮询”;忽略系统唤醒机制。
16. async/await 解决了什么问题?
问题: Swift Concurrency 的 async/await 和回调地狱相比,改变了什么?
考点: 顺序化写法、异步边界、错误传播、可读性。
回答要点:async/await 让异步代码看起来像同步顺序代码,减少嵌套回调和状态切换。它并没有消灭异步,而是把异步边界显式化,同时保留挂起、恢复和并发调度能力。异常也能通过 throw 语义自然传播。
高频追问:await 一定会切线程吗?
易错点: 把 async 当成“自动开线程”;忽略挂起点才是关键。
17. Task 是什么?Task.detached 又是什么?
问题: Task 和 GCD 的 block 有什么本质区别?
考点: 结构化并发、任务上下文、优先级、取消传播。
回答要点: Task 是 Swift Concurrency 的任务单元,具备上下文、优先级、取消等语义。async let 和 TaskGroup 属于结构化并发;直接写 Task {} 创建的是非结构化任务,但通常会继承当前 actor 上下文、优先级和任务局部值等部分信息。Task.detached 则更独立,不继承当前 actor 上下文和结构化关系,适合真正隔离的后台任务。和 GCD block 相比,Task 更强调生命周期、取消语义和可组合性。
高频追问: Task 的取消是自动的吗?Task {} 和 Task.detached {} 的差别是什么?
易错点: 把 Task 当成“新版 async block”;忽略任务取消和上下文继承。
18. TaskGroup 适合解决什么问题?
问题: 为什么有了 async let,还需要 TaskGroup?
考点: 动态并发、任务数量不确定、结果聚合。
回答要点:async let 适合少量、静态已知的并发子任务;TaskGroup 适合数量不固定、运行时动态决定的一组并发任务。它可以边创建边收集结果,等所有子任务完成后统一处理,逻辑上更像结构化版的并发 group。
高频追问:withTaskGroup 和 GCD group 有什么区别?
易错点: 把 TaskGroup 当作普通集合;忽略它是结构化并发的一部分。
19. actor 解决了什么并发问题?
问题: actor 和锁有什么区别?为什么它能保证数据安全?
考点: 隔离状态、串行访问、引用语义下的并发安全。
回答要点: actor 通过“隔离内部可变状态”来避免数据竞争。访问 actor 的隔离状态通常需要跨异步边界进入;对同一 actor 的隔离代码,系统会串行化访问。需要注意 actor 是可重入的,方法在 await 挂起点之间可能被其他任务插入执行,所以它不是“整段代码绝不穿插”的锁替代品,而是语言层面的隔离模型。
高频追问: actor 内部还能不能并发执行?actor 和 class 的最大差异是什么?
易错点: 以为 actor 能替代所有锁;忽略跨 actor 调用仍然需要 await。
20. MainActor 的作用是什么?
问题: 为什么 Swift Concurrency 里会有 MainActor?
考点: 主线程隔离、UI 状态一致性、线程切换语义。
回答要点:MainActor 用来标记必须在主执行上下文处理的代码,典型是 UI 状态更新。它提供的是“主线程相关的隔离保证”,让你不用手动到处 DispatchQueue.main.async。它的价值在于把“必须在主线程上做”的约束写进类型系统和调用语义里。
高频追问:@MainActor 方法里还能不能调用普通异步函数?
易错点: 把 MainActor 简化成“主线程宏”;以为它只影响 UI,不影响并发语义。
21. Swift Concurrency 里的取消、优先级、任务局部值怎么理解?
问题: Task 的取消是不是强制终止?优先级会自动传播吗?
考点: 协作式取消、优先级传递、任务上下文。
回答要点: Swift Concurrency 的取消是协作式的,系统只是设置取消标记,任务需要主动检查并响应。优先级可以在一定程度上传播,但不是绝对硬性抢占。任务局部值则用于在任务树里传递上下文信息,类似“异步版上下文环境”。
高频追问: 为什么说 Swift Concurrency 不是抢占式中断模型?
易错点: 认为 cancel 一调用就会立刻停;认为优先级等于 CPU 抢占级别。
22. 旧并发模型和 Swift Concurrency 的联系是什么?
问题: GCD、NSOperation、RunLoop 和 Swift Concurrency 是替代关系吗?
考点: 底层调度不变、抽象升级、兼容迁移。
回答要点: 它们不是简单替代,而是抽象层级不同。底层仍然依赖系统线程、队列、RunLoop 和调度机制;Swift Concurrency 提供的是更高层的任务和结构化并发抽象。旧模型更自由也更容易出错,新模型更安全、更可组合,但也有迁移成本。
高频追问: Swift Concurrency 会不会完全取代 GCD?
易错点: 把“新”理解成“废弃旧”;忽略项目里大量历史代码仍要共存。
23. 旧并发代码如何和 async/await 互相桥接?
问题: 现有 GCD、回调、Operation 代码怎么接入 Swift Concurrency?
考点: 迁移策略、封装边界、异步适配。
回答要点: 实际迁移通常不是一次性重写,而是先在边界层做适配。可以把回调型 API 包装成 async 方法,把旧的调度逻辑收敛到少数桥接层,再逐步向内迁移。关键是把“线程调度细节”从业务逻辑里剥离出来,让上层只感知 async 接口。
高频追问: 什么时候不值得迁移到 Swift Concurrency?
易错点: 把迁移理解成机械替换语法;忽略测试覆盖和回归风险。
24. 面试里如何快速判断该用哪种并发手段?
问题: 给你一个并发需求,你如何在 GCD、Operation、锁、actor 之间选择?
考点: 场景匹配、复杂度控制、可维护性。
回答要点: 简单一次性异步任务优先 GCD;需要依赖、取消、队列管理时优先 OperationQueue;共享可变状态的短临界区用锁;读多写少且需要线程安全抽象时可考虑读写锁或 barrier;Swift Concurrency 项目里优先考虑 async/await、Task、actor,把“共享状态最小化”作为设计原则。
高频追问: actor 和锁怎么选?
易错点: 只按“性能”选,不看语义和维护成本;把所有问题都硬塞给一种工具。
速记版
pthread 是最底层线程接口,NSThread 是封装,GCD 解决任务调度,NSOperation 解决任务编排。sync 会阻塞当前线程,async 只负责提交。串行队列保顺序,并发队列保并发度但不保证“真并行”。
barrier 用于并发队列上的读写互斥,group 用于聚合异步任务,semaphore 用于限流或同步。死锁看等待环,活锁看一直重试,优先级反转看低优先级持有高优先级需要的资源。内存可见性本质是“别的线程什么时候能看到我的写入”。
RunLoop 负责线程事件循环,mode 负责场景隔离,Timer 和 CADisplayLink 都依赖 RunLoop 触发。Swift Concurrency 的核心是 async/await、Task、TaskGroup、actor、MainActor;它不是简单替代旧并发,而是把并发模型提升到任务、结构化和隔离语义层面。