原文链接 , 译文链接, 译者:dabaosod011 ,校对:梁海舰
其他一些例子
我们对数据竞争的定义相当严格:必须以确切方式去执行原始、未经转换的程序中并行的冲突操作。引入有害的数据竞争将给编译器带来无法“打断”程序的负担。
举例来说,考虑如下程序,x和y是两个普通变量,初始化值均为0:
线程 1 |
线程2 |
if (x != 0) y = 1; |
y = 2; |
这个例子并不会导致数据竞争的出现:x变量永远都是0,因此线程1的 y = 1语句永远都不会执行。
对于下面这个例子也同样如此:
线程 1 |
线程2 |
if (x != 0) y = 1; |
if (y != 0) x = 1; |
跟上面例子一样,这两个线程同样不存在数据竞争的问题。
(这两个例子如果按照POSIX标准来看并不那么清晰,是否存在数据竞争会有一些争议。我们坚持认为不存在的理由就是考虑到,数据竞争只有在顺序一致性的执行过程中才会发生)
(译者注:POSIX强调的是,如果上述程序的执行结果依赖于两个线程的交叉执行顺序,则存在数据竞争)
然而,如果我们稍微改变一下第一个例子,就会导致数据竞争的出现了:
线程 1 |
线程2 |
y = ((x != 0)? 1 : y) |
y = 2; |
尽管在单线程运行时,上述两个例子的线程1 是等价的。但在多线程运行时就不一样了,在新版本的例子中y的值需要依情况而定。在特定情况下,y的值有可能在线程2执行的同时被改写。由于线程1的代码往往会被分割为几步执行,这种数据竞争隐含着很容易出现的问题,如果上述代码按照如下的顺序执行:
tmp = ((x != 0)? 1 : y); // tmp = 0
y = 2;
y = tmp; // y = 0
这将导致非预期性的结果,而这在最初的例子中是不会出现的。因此编译器不能以这种方式去生成代码,尽管硬件的条件转移指令会让这种代码执行速度更快,或者可以使循环语句被矢量化(译者注:https://wiki.engr.illinois.edu/download/attachments/114688007/9-Vectorization.pdf)。如果需要这种快速的版本,要么明确地按照程序的执行顺序生成代码,要么需要以某种形式让编译器能够决定哪些变量不会被共享访问。
一些其他常见的编译器优化技术也会潜在的引入数据竞争。例如考虑下面这个循环语句,用来统计list中负数的数量,其中count是一个全局或者静态类成员:
void count_neg(T *q)
{
for (T*p = q; p != 0; p = p -> next;)
{
if (p -> data < 0) ++count;
}
}
这段代码很明显会引入数据竞争,如果当前线程正在更新count值的同时,有另一个线程也刚好访问到这个变量。
编译器有可能会将count存放在某个临时变量里,然后将代码转换为以下形式:
void count_neg(T *q)
{
int local_count = count;
for (p = q; p != 0; p = p -> next;)
{
if (p -> data < 0) ++local_count;
}
count = local_count;
}
但这也会引入数据竞争。假设pos_list指向某个list,它包含两个data值:1和2, 然后声明count为全局变量并初始化为0。接下来有两个线程分别执行:
线程 1 |
线程2 |
count_neg(pos_list); |
count++; |
先前没有使用临时变量版本的程序不会存在数据竞争,因为pos_list并不包含负数,因此线程1不会读也不会改写全局变量count。
(有些人可能会觉得关于数据竞争的这种定义,没有必要苛刻要求按照特定的执行顺序,因此这就需要考虑传递给函数的一些特定的参数。尤其是在实际程序中需要传递指针参数时,这是唯一讲得通的定义,否则任何通过指针来写数据的函数都会引入数据竞争了。)
上面这个被编译器优化后的程序(引入临时变量local_count),线程1不管何时都存在对count的写操作,因此也就引入了数据竞争。
(尽管在Java和C++0x中都将这种转换清楚地列为非法,然而在POSIX中却没有明确规定,而且在当前(2008年)许多C编译器中都存在这种转换。)
需要注意的是,至少在没有异常出现的情况下,上述代码可允许被按如下方式转换:
void count_neg(T *q)
{
int local_count = 0;
for (p = q; p != 0; p = p -> next;)
{
if (p -> data < 0) ++local_count;
}
if (local_count != 0) count += local_count;
}