12. 并发 Bugs

背景回顾:当我们在写程序的时候,我们其实是在写 bug。时至今日,我们还没有有效、方便的技术能帮助我们快速构建可靠的软件系统。并发 bugs “若隐若现” 的特性又导致它们经常逃脱开发人员的掌控。

本讲内容:常见的并发 bugs:死锁、数据竞争、原子性和顺序违反。

12.1 致命的并发 Bugs

历史上,还有许多并发性导致的严重事故,包括 2003 年美加大停电,估计经济损失 250 亿—300 亿美元。并发 bug 难捉摸的重要原因之一来自它触发的不确定性,即便经历严格的测试,仍有罕见的调度可能导致连锁反应;直到 2010 年左右,学术界和工业界才对并发系统的正确性开始有了系统性的认识。

12.2 死锁

12.3 数据竞争

相当多的并发 bugs 最终都会体现为数据竞争。对于并发编程的初学者,除了在主观上避免数据竞争之外,也要记得忘记上锁、上错锁、在临界区外访问共享资源都可能导致数据竞争。

12.4 原子性/顺序违反

实际上,“原子性” 一直是并发控制的终极目标。对编程者而言,理想情况是一段代码的执行要么看起来在瞬间全部完成,要么好像完全没有执行过。代码中的副作用:共享内存写入、文件系统写入等,则都是实现原子性的障碍。

因为 “原子性” 如此诱人,在计算机硬件/系统层面提供原子性的尝试一直都没有停止过:从数据库事务 (transactions, tx) 到软件和硬件支持的 Transactional Memory (“an idea ahead its time”) 到 Operating System Transactions,直到今天我们依然没有每个程序员都垂手可得的可靠原子性保障。

而保证程序的执行顺序就更困难了。Managed runtime 实现自动内存管理、channel 实现线程间通信等,都是减少程序员犯错的手段。

Take-away Messages

人类本质上是 sequential creature,因此总是通过 “块的顺序执行” 这一简化模型去理解并发程序,也相应有了两种类型的并发 bugs:

  • Atomicity violation,本应原子完成不被打断的代码被打断
  • Order violation,本应按某个顺序完成的未能被正确同步

与这两类 bugs 关联的一个重要问题是数据竞争,即两个线程同时访问同一内存,且至少有一个是写。数据竞争非常危险,因此我们在编程时要尽力避免。

课后习题/编程作业

📚阅读材料

教科书 Operating Systems: Three Easy Pieces

  • 第 32 章 - Concurrency Bugs