基于库实现多线程的局限

最近在看C++ Memory Model, 顺便读了Hans-J. Boehm的10年前的paper Threads Cannot be Implemented as a Library
C++多线程库有很多,但Boehm指出,C++的多线程库大多数情况是OK的,但在有些情况下是有问题的,并列举了三种情况。此外,因库隐藏了底层现实,在追求极致性能的地方使用库有限制。Boehm的结论是:C++必须在语言层面上支持多线性。正因Boehm和Herb Sutter的努力,C++11支持了多线程。

Boehm大叔以POSIX的规范pthread为例,进行详细的论述,简单总结如下:

##1. 正确性问题
1.1 第一种情况。

1
2
3
x = y = 0; //shared variables
Thread1: Thread2:
if (x == 1) ++y; if (y == 1) ++x;

这是合法的pthreads程序,初看,没有data race,输出是 x == 0, y ==0. 但是compiler是不知道多线程的,可能有如下优化:

1
2
Thread1: Thread2:
++y; if (x != 1) --y; ++x; if (y != 1) --x;

这下,有了data race, 可能输出 x==1, y==1.

1.1 第二种情况

1
2
3
4
struct { char a; char b; char c; char d; char e; char f; char g; char h; } x;
Thread 1: Thread 2:
x.a = 'a'; x.b='b';

用pthread实现上面程序,合法,看起来也没有data race。 但是,如果没有语言层面的支持,compiler并不知道多线程,可能把8个byte一次性读写。 这样又有了data race问题。

1.3 第三种情况, Register promotion

1
2
3
4
5
6
for (...) {
...
if (mt) pthread_mutex_lock(...);
x = ... x ...
if (mt) pthread_mutex_unlock(...);
}

初看,有pthread mutex的保护,可以无忧了。但是,在compiler眼里,pthread_mutex_lock ( ) 和pthread_mutex_unlock ( ) 是透明的,只不过是两个普通函数调用而已,于是,compiler把x从循环里弄出来,放到寄存器里去,提高效率, 于是又悲剧了…

1
2
3
4
5
6
7
8
9
10
11
12
r = x; //pseudo-code, r is a register
for (...) {
...
if (mt) {
x = r; pthread_mutex_lock(...); r = x;
}
r = ... r ...
if (mt) {
x = r; pthread_mutex_unlock(...); r = x;
}
}
x = r;

##2. 性能问题
pthread_mutex_lock ( )和pthread_mutex_unlock ( )一般要求一个硬件的atomic memory update指令,为了阻止CPU的的reordering,可能还需要一个memory barrier指令,这些指令的成本百倍于register-to-register指令。这样,基于库的多线程不用使用lock-free/wait-free技术追求更好的性能。

forhappy同学收集了一个不错的C++ 多线程与内存模型资料汇. 看看。