CppCoreGuidelines CP.2 避免数据竞争
19 June 2023
“Avoid data races”
理由
除非你确实做到了,不然的话,无法保证代码是否正确工作,一些莫名其妙的错误会一直在那。
注意
总归一句话,如果两个线程要同时访问同一个对象,在没有同步机制的情况下,至少一个线程是写线程(进行一些非 const
的操作),你就会遇到数据竞争的情况。
坏例子
有很多数据竞争的例子,不少目前还运行在产品软件中。一个简单的例子:
int get_id() { static int id = 1; std::this_thread::sleep_for(std::chrono::milliseconds(30)); return id++; } int main() { std::thread t1(get_id); std::thread t2(get_id); std::thread t3(get_id); t1.join(); t2.join(); t3.join(); std::cout << get_id(); }
3
此处的自增是数据竞争的一个例子。可能会出现以下这些错误:
- 线程 A 加载 id 的值,然后操作系统从 A 切换到其他线程,在此过程,其他线程把 id 的值增加了 100 次,然后操作系统切换到 A,A 继续把增加的 id 值 2 写入 id。
- 也可能,线程 A 和 B 加载 id,同时增加 id 的值,这时候,A 和 B 得到的 id 值是一样的。
局部的静态变量是数据竞争的常见根源。
坏例子:
void f(fstream& fs, regex pattern) { array<double, max> buf; int sz = read_vec(fs, buf, max); // read from fs into buf gsl::span<double> s {buf}; // ... auto h1 = async([&] { sort(std::execution::par, s); }); // spawn a task to sort // ... auto h2 = async([&] { return find_all(buf, sz, pattern); }); // spawn a task to find matches // ... }
这里,我们发现 buf 的元素之间存在数据竞争(sort 会读取和写入数据)。所有的数据竞争都很讨厌。这里,我们发现程序栈中存在数据竞争。并非所有的数据竞争都是这么容易发觉的。
坏例子:
// code not controlled by a lock unsigned val; if (val < 5) { // ... other thread can change val here ... switch (val) { case 0: // ... case 1: // ... case 2: // ... case 3: // ... case 4: // ... } }
因为编译器不知道 val
的值会被改变,所以,在编译实现的过程中,可能使用
5 个地址的跳转表的方式实现。然后,如果 val
的值超出了 0 到 4 的范围的话,程序可能就跳转到某个错误的地方继续执行。实际上,更恐怖的是,通过检查生成的代码,你可能能设计出某个条件,让程序跳转到你想要执行的地方。这样就产生了一个安全漏洞。
强化
有些商业或开源的软件能尝试解决这个问题,但是,请注意,这会产生额外成本,也会有一些盲点。静态工具通常会产生很多误报,而运行时进行检测的工具又非常昂贵。我们希望有更好的工具。用多个工具,通常能比单个工具捕捉更多的问题。
其他一些方式可以避免数据竞争:
- 避免全局数据
- 避免静态变量
- 在程序栈中用更多具体类型,而不要指针传来传去
- 用不可变数据,比如字面量(literal)、
constexpr
以及const