如何解决数据竞争(Data Racing)

在多线程编程中, 有两个需要注意的问题, 一个是数据竞争, 另一个是内存执行顺序.

什么是数据竞争(Data Racing)

我们先来看什么是数据竞争(Data Racing), 数据竞争会导致什么问题.

#include <iostream>#include <thread>int counter = 0;void increment() {    for (int i = 0; i < 100000; ++i) {        // ++counter实际上3条指令        // 1. int tmp = counter;        // 2. tmp = tmp + 1;        // 3. counter = tmp;        ++counter;    }}int main() {    std::thread t1(increment);    std::thread t2(increment);    t1.join();    t2.join();    std::cout << "Counter = " << counter << "\n";    return 0;}

在这个例子中, 我们有一个全局变量counter, 两个线程同时对它进行增加操作. 理论上, counter的最终值应该是200000. 然而, 由于数据竞争的存在, counter的实际值可能会小于200000.

这是因为, ++counter并不是一个原子操作, CPU会将++counter分成3条指令来执行, 先读取counter的值, 增加它, 然后再将新值写回counter. 示意图如下:

Thread 1                  Thread 2---------                 ---------int tmp = counter;        int tmp = counter;tmp = tmp + 1;            tmp = tmp + 1;counter = tmp;            counter = tmp;

两个线程可能会读取到相同的counter值, 然后都将它增加1, 然后将新值写回counter. 这样, 两个线程实际上只完成了一次+操作, 这就是数据竞争.

如何解决数据竞争

c++11引入了std::atomic, 将某个变量声明为std::atomic后, 通过std::atomic的相关接口即可实现原子性的读写操作.

现在我们用std::atomic来修改上面的counter例子, 以解决数据竞争的问题.

#include <iostream>#include <thread>#include <atomic>// 只能用bruce-initialization的方式来初始化std::atomic<T>.// std::atomic<int> counter{0} is ok, // std::atomic<int> counter(0) is NOT ok.std::atomic<int> counter{0}; void increment() {    for (int i = 0; i < 100000; ++i) {        ++counter;    }}int main() {    std::thread t1(increment);    std::thread t2(increment);    t1.join();    t2.join();    std::cout << "Counter = " << counter << "\n";    return 0;}```
我们将int counter改成了std::atomic<int> counter, 使用std::atomic<int>的++操作符来实现原子性的自增操作.std::atomic提供了以下几个常用接口来实现原子性的读写操作, memory_order用于指定内存顺序, 我们稍后再讲.

`// 原子性的写入值` `std::atomic<T>::store(T val, memory_order sync = memory_order_seq_cst);` `// 原子性的读取值` `std::atomic<T>::load(memory_order sync = memory_order_seq_cst);` `// 原子性的增加` `// counter.fetch_add(1)等价于++counter` `std::atomic<T>::fetch_add(T val, memory_order sync = memory_order_seq_cst);` `// 原子性的减少` `// counter.fetch_sub(1)等价于--counter` `std::atomic<T>::fetch_sub(T val, memory_order sync = memory_order_seq_cst);` `// 原子性的按位与` `// counter.fetch_and(1)等价于counter &= 1` `std::atomic<T>::fetch_and(T val, memory_order sync = memory_order_seq_cst);` `// 原子性的按位或` `// counter.fetch_or(1)等价于counter |= 1` `std::atomic<T>::fetch_or(T val, memory_order sync = memory_order_seq_cst);` `// 原子性的按位异或` `// counter.fetch_xor(1)等价于counter ^= 1` `std::atomic<T>::fetch_xor(T val, memory_order sync = memory_order_seq_cst);`

改用 Rust 即可。

好的,老哥,我都搞搞看看