使用volatile的多线程代码的明显不安全行为的真实示例 我已经阅读了很多,并说明了为什么 Value不能使多线程C++代码安全。

使用volatile的多线程代码的明显不安全行为的真实示例 我已经阅读了很多,并说明了为什么 Value不能使多线程C++代码安全。,c++,multithreading,volatile,C++,Multithreading,Volatile,我理解原因,我认为理解可能的危险,我的问题是我无法创建或找到任何示例代码,也无法提及使用它进行同步的程序会产生实际可见的错误或意外行为的情况。我甚至不需要它是可复制的(因为即使进行了优化,当前的编译器似乎也会尝试生成安全的代码),这只是一个实际发生的示例。请看以下示例: 两个线程使用相同的函数递增一个变量。如果未定义USE_ATOMIC,则增量本身将在var的原子副本中执行,因此增量本身是线程安全的。但是正如您所看到的,对volatile变量的访问是不允许的!如果在不使用_原子的情况下运行示例,

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

请看以下示例:

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

发生的事情很简单:
volatile
仅仅意味着可以在编译器无法控制的情况下更改变量。这意味着,编译器必须在修改和写回结果之前读取变量。但这与同步无关。更重要的是:在多核CPU上,变量可以存在两次(例如在每个缓存中),并且不进行cachs同步!在基于线程的编程中,还有很多事情必须认识到。这里
内存障碍
是缺少的主题

#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;
}
#包括
#包括
#包括
#包括
//#定义和使用原子
#ifdef使用_原子
std::原子i{0};
#否则
波动长i=0;
#恩迪夫
常量长CNT=10000000;
void公司(波动性长期和风险值)
{
std::原子局部拷贝{var};
本地拷贝++;
var=本地拷贝;
}
void func1()
{
长n=0;
而(nstd::cout假设您有一个计数器,您希望使用它跟踪某个操作完成的次数,每次递增计数器

如果在多个线程中运行此操作,则除非计数器是
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
launch策略强制异步启动。每个任务只需将计数器增加100万次。完成这两个任务后,我们预计计数器为200万

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

如果我们将计数器更改为
std::atomic
,我们将得到预期的行为

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


有关更多信息,请参阅Scott Meyers的文章。

可复制、简单且意义重大。不知道我为什么没有想到这一点。谢谢。