19 June 2023

C++ 核心指南目录

“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