具有volatile的多线程代码的明显不安全行为的真实示例
Real world example of noticeable unsafe behavior of multithreaded code with volatile
我已经读了很多回答和文章,说明为什么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;
}
- C++/CLI 和 C#/VB 与不安全和外部有什么区别?
- 问:Apache Arrow 数组生成器不安全追加
- 不安全的 MPI 非阻塞通信示例?
- 有没有一种简单的方法来检查C++中的不安全表达式
- 为什么静态向下转换unique_ptr不安全?
- 哪些整数操作不安全
- 为什么这个递归 lambda 函数不安全?
- 解决方法:QPixmap:在GUI线程之外使用pixmap是不安全的
- 正在匹配不安全的正则线程
- 如何修复编译错误"此函数或变量可能不安全"(strcpy)
- 编译器在 const ref 类型参数上使用临时对象时是否应该警告不安全的行为?
- 实现没有不安全服务器凭据的自定义 AuthMetadataProcessor
- 什么时候关闭__strict_ansi__标志是不安全的
- 原子对象在普通对象安全的任何上下文中都是不安全的
- OpenSSL:将不安全的BIO提升为安全
- 这是对支撑初始器列表的不安全使用情况
- 从 C# 到C++和返回的数组,没有不安全的代码
- 一个线程设置成员,而另一个循环上方 - 是此螺纹 - 不安全
- 访问"std::variant"的不安全、"noexcept"和无开销方式
- 具有volatile的多线程代码的明显不安全行为的真实示例