201250093 谭子悦
我选择阅读的论文是 《Threads Cannot be Implemented as a Library》。
首先,我之所以会选择阅读这篇论文,不是因为它被排在了阅读列表第一位,而是他的标题吸引了我。
在操作系统课程的学习中,我就曾疑惑:为什么 C 语言的标准库里没有多线程 API?后来我才了解到,C 原生并不支持多线程,需要通过与平台相关的第三方库实现,比如最常见的实现接口就是 Linux 下的 POSIX Threads API,即 Pthreads。
这篇论文则提出:多线程不能仅通过库层面来实现,而要建立语言层面的内存模型定义。
这篇文章主要从 Pthreads 的实现思路、存在问题和性能三个角度进行了探讨。
Pthreads 在定义语义时并没有构建形式化的内存模型,而是通过一种 “程序员友好” 的方式规定了 “使用 Pthreads 的程序不应该有对同一内存地址的并发访问”,即禁止了会导致数据竞争的使用。同时,Pthreads 还定义了若干同步函数。
为什么这样的定义会产生问题呢?编译器在优化、编译代码时,它只了解到对同步函数有哪些操作是不可以做的(如改变顺序、缩小范围等),但在没有被同步函数包围,即不被 “锁” 保护的代码区域,它的行为是完全自由的 —— 因为编译器没有 “并发” 的概念,任何在线性执行下合理的优化都将被允许。这就导致在存在数据竞争的区域,并发执行下程序的输出结果是完全未知的。
为了说明这些问题,文章中举了三个层层递进的案例。
第一个案例是最基本的 “无中生有”:在原本编写的代码中并不存在数据竞争,但在编译器优化后反而引入了竞争。
第二个案例是相邻数据写入:编译器可能会在优化时将相邻地址空间的数据写入综合为一次写入,而这也引入了数据竞争。
第三个案例是寄存器推广:一种更为复杂的优化情况,同样会引入明显的数据竞争。
在初次阅读到这里时,我并没有很好地理解到这些问题的关键点。当时我觉得,既然数据竞争危害那么大,那我们直接用 “一把大锁保平安”,完全杜绝潜在的数据竞争不就可以了吗?而在进一步阅读后,我理解了这些问题。
由文章在性能测试中给出的数据可见,锁(尤其是互斥锁)的开销是巨大的,且性能差距随线程数量越小越大。在实际开发中,我们的程序并不一定在任何场合下都一定要多线程运行,甚至可能大多数情况下都在单线程运行。如果用 “锁” 暴力保证多线程安全,结果是对了,但会显著地拖慢程序的运行效率,是得不偿失的。
而由于上文所说的问题,我们无法预测在有数据竞争下程序的可能结果,也就难以设计高效且正确的 lock-free 程序。因此,多线程不能仅通过库的层面来实现。
在操作系统、数据库等课程中,并发相关问题通常都需要用锁来解决,因此久而久之也就让我默认 “只有上锁才能解决并发问题”。而这篇论文则让我对并发问题有了新的认识:好的并发算法设计总离不来数据竞争问题,虽然可以用到这些算法的场合有限,但它们许多都和各种库的底层实现息息相关,任何性能改进都可能会对使用者带来巨大的性能提升。因此,为了实现这些算法,语言层面的多线程支持,即语言层面的内存模型语义,是必不可少的。