具有volatile的多线程代码的明显不安全行为的真实示例

Real world example of noticeable unsafe behavior of multithreaded code with volatile

本文关键字:真实 不安全 volatile 多线程 代码 具有      更新时间:2023-10-16

我已经读了很多回答和文章,说明为什么volatile不能使多线程c++代码安全。

我理解推理,我认为理解可能的危险,我的问题是我无法创建或找到任何示例代码或提及使用它进行同步的程序产生实际可见的错误或意外行为的情况。我甚至不需要它是可重复的(因为当前的编译器即使经过优化似乎也会尝试产生安全的代码),只是一个真正发生的例子。

假设你有一个计数器,你想用它来跟踪某个操作完成了多少次,每次增加计数器。

如果你在多线程中运行这个操作,那么除非计数器是std::atomic或者被锁保护,否则你会得到意想不到的结果,volatile不会有帮助。

下面是一个简化的例子,至少对我来说,它再现了不可预测的结果:

#include <future>
#include <iostream>
#include <atomic>
volatile int counter{0};
//std::atomic<int> counter{0};
int main() {
    auto task = []{ 
                      for(int i = 0; i != 1'000'000; ++i) {
                          // do some operation...
                          ++counter;
                      }
                  };
    auto future1 = std::async(std::launch::async, task);
    auto future2 = std::async(std::launch::async, task);
    future1.get();
    future2.get();
    std::cout << counter << "n";
}

现场演示。

这里我们使用std::async启动两个任务,使用std::launch::async启动策略强制其异步启动。每个任务只是将计数器增加一百万次。在这两个任务完成后,我们期望计数器是200万。

然而,增量操作是在读取计数器和写入计数器之间的读写操作,另一个线程也可能对它进行了写入,并且增量操作可能会丢失。从理论上讲,因为我们已经进入了未定义行为的领域,任何事情都有可能发生!

如果我们将计数器更改为std::atomic<int>,我们将得到我们期望的行为。

同样,假设另一个线程使用counter来检测操作是否已经完成。不幸的是,没有什么可以阻止编译器重新排序代码,并在完成操作之前增加计数器。同样,这可以通过使用std::atomic<int>或设置必要的内存围栏来解决。

参见Scott Meyers的Effective Modern c++获取更多信息

看下面的例子:

两个线程用同一个函数增加一个变量。如果没有定义USE_ATOMIC,则增量本身将在var的原子副本中完成,因此增量本身是线程安全的。但是正如您所看到的,对volatile变量的访问不是!如果不使用USE_ATOMIC运行示例,则结果是未定义的。如果设置了USE_ATOMIC,结果总是相同的!

发生的事情很简单:volatile仅仅意味着变量可以在编译器的控制之外进行更改。这意味着,编译器必须在修改和回写结果之前读取变量。但这与同步没有任何关系。更重要的是:在多核CPU上,变量可以存在两次(例如,在每个缓存中),并且没有缓存同步完成!在基于线程的编程中,有很多东西必须被认识到。这里memory barrier是缺失的主题。

#include <iostream>
#include <set>
#include <thread>
#include <atomic>
//#define USE_ATOMIC
#ifdef USE_ATOMIC
std::atomic<long> i{0};
#else
volatile long i=0;
#endif
const long cnts=10000000;
void inc(volatile long &var)
{
    std::atomic<long> local_copy{var};
    local_copy++;
    var=local_copy;
}
void func1()
{
    long n=0;
    while ( n < cnts )
    {
        n++;
#ifdef USE_ATOMIC
        i++;
#else
        inc( i );
#endif
    }
}

int main()
{
    std::thread t1( func1 );
    std::thread t2( func1 );
    t1.join();
    t2.join();
    std::cout << i << std::endl;
}