17. 并发 Bugs

理论上说,我们可以用并发编程 API (线程、互斥、同步机制) 实现几乎一切实用的并发程序。然而,并发程序 “难写” 也是非常出名的。时至今日,我们还没有有效、方便的技术能帮助我们快速构建可靠的软件系统。并发 bugs “若隐若现” 的特性又导致它们经常逃脱开发人员的掌控。

本讲内容:并发编程时常见的错误模式:数据竞争、死锁、原子性和顺序违反。

17.1 致命的并发 Bugs 和数据竞争

相当多的并发 bugs 最终都会体现为数据竞争。对于并发编程的初学者,除了在主观上避免数据竞争之外,也要记得忘记上锁、上错锁、在临界区外访问共享资源都可能导致数据竞争。历史上,还有许多并发性导致的严重事故,包括 2003 年美加大停电,估计经济损失 250 亿—300 亿美元。并发 bug 难捉摸的重要原因之一来自它触发的不确定性,即便经历严格的测试,仍有罕见的调度可能导致连锁反应;直到 2010 年左右,学术界和工业界才对并发系统的正确性开始有了系统性的认识。至于如何消灭并发 bugs?让我们看看 AI 的回答吧!

💬
Prompt: ... 如何消灭并发 bugs?

大语言模型有一个非常显著的特点 (和 self-attention 机制有关):文本中的文字会很大程度引导内容的生成——即便你只是让他 ”看看 AI 的回答“,它也会被引导向 AI 的方向。Reasoning model 通过吐出额外的 tokens 来产生直接生成无法实现的逻辑推理。

17.2 死锁

17.3 原子性/顺序违反

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

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

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

17.4 总结

Take-away messages: 人类本质上是 sequential creature,因此总是通过 “块的顺序执行” 这一简化模型去理解并发程序,也因此带来了数据竞争、Atomicity violation (本应原子完成不被打断的代码被打断)、Order violation (本应按某个顺序完成的未能被正确同步) 等问题。数据竞争非常危险,我们在编程时要尽力避免。

课后习题/编程作业

📚阅读材料

教科书 Operating Systems: Three Easy Pieces

  • 第 32 章 - Concurrency Bugs