C++ 我应该为在不同线程之间共享内存的每个对象指定volatile关键字吗
我刚刚在CERT网站上读到一篇文章,注意到编译器理论上可以优化以下代码,在寄存器中存储C++ 我应该为在不同线程之间共享内存的每个对象指定volatile关键字吗,c++,multithreading,volatile,C++,Multithreading,Volatile,我刚刚在CERT网站上读到一篇文章,注意到编译器理论上可以优化以下代码,在寄存器中存储标志变量,而不是修改不同线程之间共享的实际内存: bool flag = false;//Not declaring as {{volatile}} is wrong. But even by declaring {{volatile}} this code is still erroneous void test() { while (!flag) { Sleep(1000); // sleeps
标志变量,而不是修改不同线程之间共享的实际内存:
bool flag = false;//Not declaring as {{volatile}} is wrong. But even by declaring {{volatile}} this code is still erroneous
void test() {
while (!flag) {
Sleep(1000); // sleeps for 1000 milliseconds
}
}
void Wakeup() {
flag = true;
}
void debit(int amount){
test();
account_balance -= amount;//We think it is safe to go inside the critical section
}
我说得对吗
我需要为程序中在不同线程之间共享内存的每个对象使用volatile
关键字,这是真的吗?不是因为它为我做了某种同步(我需要使用互斥体或任何其他同步原语来完成这样的任务)但正是因为编译器可能会优化我的代码并将所有共享变量存储在寄存器中,这样其他线程就永远不会得到更新的值了?这不仅仅是将它们存储在寄存器中,在共享主内存和CPU之间有各种级别的缓存。大部分缓存都是针对每个CPU核心的,因此在那里进行的任何更改在很长一段时间内都不会被其他核心看到(或者,如果其他核心正在修改相同的内存,那么这些更改可能会完全丢失)
对于缓存的行为没有任何保证,即使对于当前的处理器来说是正确的,但对于较旧的处理器或下一代处理器来说也可能不是正确的。为了编写安全的多线程代码,您需要正确地编写。最简单的方法是使用提供的库和工具。尝试使用volatile等低级原语自己完成这项工作是一件非常困难的事情,涉及到大量深入的知识。关于您的特定
布尔标志=假
例如,将其声明为volatile将普遍有效,并且是100%正确的。
但它不会一直为你买单
Volatile要求编译器直接在内存/寄存器上执行对象(或仅仅是C变量)的每次求值,或者在从外部内存介质检索到内部内存/寄存器之前执行。在某些情况下,代码和内存占用的大小可能相当大,但真正的问题是这还不够
当一些基于时间的上下文切换正在进行时(例如线程),并且您的易失性对象/变量已对齐并适合CPU寄存器,您将得到您想要的结果。在这些严格的条件下,更改或评估是原子地完成的,因此在上下文切换场景中,另一个线程将立即“意识到”任何更改
但是,如果您的对象/大变量不适合CPU寄存器(从大小或无对齐),volatile上的线程上下文开关可能仍然是no-no。。。并发线程上的求值可能会捕获中间更改过程。。。e、 g.在更改5成员结构副本时,并发线程在第3个成员更改时被调用cabum强>
结论是(回到“操作系统101”),您需要识别您的共享对象,选择抢占+阻塞或非抢占或其他并发资源访问策略,并使您的评估器/更改器原子化。访问方法(change/eval)通常包含make-atomic策略,或者(如果它是对齐的并且很小的话)简单地将其声明为volatile。它实际上非常简单,但同时也令人困惑。在高层次上,当你编写C++代码编译器和CPU时,有两个优化实体在发挥作用。在编译器中,关于变量访问有两种主要的优化技术——忽略变量访问(即使是在代码中编写的),以及围绕这个特定的变量访问移动其他指令
具体而言,以下示例演示了这两种技术:
int k;
布尔旗
void foo() {
flag = true;
int i = k;
k++;
k = i;
flag = false;
}
在提供的代码中,编译器可以自由跳过对标志的第一次修改,只留下最后的赋值为false;并完全删除对k的任何修改。若将k设为volatile,则需要编译器保留对k=的所有访问权限,k=将递增,并比原始值放回。如果您也将标志设置为volatile,那么代码中将保留两个赋值,第一个赋值为true,第二个赋值为false。然而,重新排序仍然是可能的,有效的代码可能看起来像
void foo() {
flag = true;
flag = false;
int i = k;
k++;
k = i;
}
如果另一个线程希望标志指示k是否正在被修改,这将产生不愉快的影响
实现预期效果的方法之一是将这两个变量定义为原子变量。这将阻止编译器进行这两种优化,确保执行的代码与编写的代码相同。请注意,原子实际上是一个volatile+,它做所有的volatile+做更多的事情
另一件需要注意的事情是,编译器优化确实是一个非常强大和理想的工具。人们不应该仅仅为了好玩而阻止它们,所以原子性应该只在需要的时候使用。那么,我是对的?我应该为每个在不同线程之间共享内存的对象指定volatile关键字吗?不,应该为您的语言和体系结构使用适当的线程结构。到处抛出volatile不是解决方案。你所说的“适当的线程结构”是什么意思?假设我启动几个线程,并通过某种同步原语(如互斥体)同步对共享变量的所有访问。在这之后我是安全的还是应该使用volatile?@timb:你说得不太对。任何全局内存位置的更改最终都将对其他线程可见。但是,根据记忆模式的不同,不能保证顺序是什么。@Frozenhart请务必听从Tim的建议。多线程是一个非常复杂的问题。使用数据结构,如单读单写队列等。以这样一种方式对问题进行建模:线程间交互通过专门的数据结构,这些数据结构可以通过设计来处理并发性。在这种情况下,编译器可以对指令重新排序