简体   繁体   English

具有函数访问的全局共享变量的C ++ volatile关键字

[英]C++ volatile keyword with global shared variable accessed by function

I have a multi-threaded C++ application. 我有一个多线程的C ++应用程序。

Now I know that for global shared variables, you should use volatile in some cases while checking the state of the variable or else the compiler could assume that the variable's value never changes (in that thread). 现在我知道,对于全局共享变量,在某些情况下应在检查变量状态时使用volatile,否则编译器可能会假定变量的值从不更改(在该线程中)。

What if, however, instead of checking the status of a variable I call a method that returns the value of the variable? 但是,如果我不检查变量的状态而是调用返回变量值的方法怎么办? For instance: 例如:

static int num = 0;

...

void foo()
{
   while(getNum() == 0)
   {
      // do something (or nothing)
   }
}

Would I still have to make num a volatile variable? 我仍然需要将num设置为volatile变量吗? or does the compiler recognize that since I'm using a method to access that variable num it won't cache the result? 还是编译器识别出由于我使用一种方法来访问该变量num而不会缓存结果?

Anyone got any ideas? 任何人有任何想法吗?

Thanks in advance, 提前致谢,

~Julian 〜朱利安

edit : inside my while loop I removed the sleep call and replaced it with something generic such as a comment to do something (or nothing) 编辑 :在我的while循环中,我删除了sleep调用,并用诸如评论之类的通用名称代替了该名称以执行某项操作(或不执行任何操作)

No, volatile is never needed as long as you're doing the necessary synchronization. 不,只要执行必要的同步,就永远不需要volatile

Calling thread library synchronization functions, whatever they are on your platform, should take care of invalidating locally "cached" values and making the compiler reloads globals. 无论您在平台上使用什么函数,调用线程库同步函数都应注意使本地“缓存”的值无效,并使编译器重新加载全局变量。

In this particular case, sleep is likely to have such an effect, but it's not a good implementation anyway. 在这种特殊情况下, sleep很可能会产生这种影响,但是无论如何这都不是一个好的实现。 There should be a condition variable on num , protect it with a setter function, and have the setter function send a signal to foo . num上应该有一个条件变量,用setter函数对其进行保护,并让setter函数向foo发送信号。

As to the specific question, whether the function hides the access from optimization is extremely implementation- and situation-dependent. 对于特定的问题,该功能是否对优化隐藏访问完全取决于实现和情况。 Your best bet is to compile the getter function in a separate invokation of the compiler, but even then, there's no way to guarantee that interprocedural optimization doesn't occur. 最好的选择是在单独的编译器调用中编译getter函数,但是即使那样,也无法保证不会发生过程间优化。 For example, some platforms may put IR code in the .o files and perform code generation in the "linker" stage. 例如,某些平台可能会将IR代码放入.o文件中,并在“链接器”阶段执行代码生成。

Disclaimer. 免责声明

Key words above: 1. as long as you're doing the necessary synchronization and 2. likely to have such an effect . 上面的关键字:1. 只要您在进行必要的同步,并且2. 可能会产生这种效果

1: sleep or an empty busy-loop are not "necessary synchronization." 1: sleep或空的繁忙循环都不是“必需的同步”。 That is not the correct way to write a multithreaded program, period. 那不是编写多线程程序的正确方法。 So, volatile may be needed in such cases. 因此,在这种情况下可能需要挥发物。

2: Yes, sleep may not be counted by the implementation an I/O function, and may even be labeled as pure and free of side-effects. 2:是的,实现I / O功能可能不会将sleep计算在内,甚至可以将其标记为纯净的且没有副作用。 In that case, volatile on the global would be necessary. 在这种情况下, volatile对全球是必要的。 However, I doubt any implementations have really been distributed which would break sleep loops like that, since they are unfortunately common. 但是,我很怀疑是否有任何实现会破坏像这样的sleep循环的实现,因为它们很常见。

What you propose is basically a misuse of "volatile", its real purpose is to tell the compiler that the variable may be modified by external hardware or another process in the system, therefore, it needs to be really read from memory everytime its used. 您所建议的基本上是对“易失性”的滥用,其真正目的是告诉编译器该变量可能由外部硬件或系统中的其他进程修改,因此,每次使用它时,都必须从内存中真正读取该变量。

It will not protect you from thread collisions etc. within your program, although in your case it looks like you are using the flag to signal a shutdown request. 它不会保护您免受程序中线程冲突等的影响,尽管在您的情况下,看起来您正在使用该标志来发出关闭请求的信号。

Its actually OK to do this without synchronising etc. provided you know that only a single controlling thread will update the variable. 只要您知道只有一个控制线程将更新该变量,则无需同步等操作即可。 Also I would use a bit manipulation to set the flag as this is more likely to be "atomic" on more hardware. 我还将使用一点操作来设置标志,因为这在更多硬件上更有可能是“原子的”。

num && x'00000001' 

Unfortunately volatile semantics are kinda wishy-washy. 不幸的是,易失性语义有点难以实现。 The concept of volatile wasn't really meant to be used for threading. volatile的概念并不是真正用于线程。

Potatoswatter is correct that calling the OS synchronization primitives will normally prevent the optimizing compiler from hoisting the read of num from the loop. Potatoswatter是正确的,因为调用OS同步原语通常会阻止优化的编译器提升循环中num的读取。 But it works for sorta the same reason that using an accessor method works... by accident. 但是它的工作原理与使用访问器方法的原因相同……是偶然的。

The compiler sees you calling a function that isn't immediately available for inlining or analysis, so it has to assume that any variable that could be used by some other function might be read or altered in that opaque function. 编译器会看到您正在调用一个不能立即用于内联或分析的函数,因此它必须假定在该不透明函数中可能读取或更改了其他函数可以使用的任何变量。 So before doing the call, the compiler needs to write all those "global" variables back to memory. 因此,在进行调用之前,编译器需要将所有这些“全局”变量写回到内存中。

At corensic, we added an inline function to jinx.h that does this in a more direct way. 在corensic,我们在jinx.h中添加了一个内联函数,以更直接的方式执行此操作。 Something like the following: 类似于以下内容:

 inline void memory_barrier() { asm volatile("nop" ::: "memory"); }

This is rather subtle, but it effectively tells the compiler (gcc) that it can't get rid of this chunk of opaque asm and that the opaque asm can read or write any globally visible piece of memory. 这是相当微妙的,但是它有效地告诉编译器(gcc)它不能摆脱这块不透明的asm,并且不透明的asm可以读取或写入任何全局可见的内存。 This effectively stops the compiler from reordering loads/stores across this boundary. 这有效地阻止了编译器跨此边界对加载/存储进行重新排序。

For your example: 例如:

memory_barrier(); memory_barrier(); while (num == 0) { memory_barrier(); while(num == 0){memory_barrier(); ... } ...}

Now the read of num is stuck in place. 现在,num的读取卡在适当的位置。 And potentially more importantly, it's stuck in place with relation to other code. 而且可能更重要的是,它与其他代码保持一致。 So you could have: 因此,您可以:

 while (flag == 0) { memory_barrier(); }  // spin
 process data[0..N]

And another thread does: 另一个线程执行:

 populate data[0..N]
 memory_barrier();
 flag = 1;

PS. PS。 If you do this type of thing (essentially creating your own synchronization primitives) the perf wins can be big but the quality risk is high. 如果您执行此类操作(本质上是创建自己的同步原语),则获胜可能会很大,但质量风险会很高。 Jinx is particularly good at finding bugs in these lock-free structures. Jinx特别擅长在这些无锁结构中发现错误。 So you might want to use it or some other tool to help test this stuff. 因此,您可能想使用它或其他工具来帮助测试这些东西。

PPS. PPS。 The linux community has a nice post about this called "volatile considered harmful", check it out. linux社区有一篇不错的文章,叫做“ volatilevolatile有害”,请查看。

technically it does need to be marked volatile. 从技术上讲,它确实需要标记为挥发性。 Compilers are free to do what they want in order to optimise code as long as it continues to conform to the c++ abstract machine spec. 编译器可以自由地做他们想做的事情,以优化代码,只要它继续符合c ++抽象机规范。 A conforming compiler, with enough resources, could inline all instances of getNum, move the value of num into a register (or simply, noticing that its never actually changed by any code, treat it as a constant) for the entire lifetime of the program. 具有足够资源的合格编译器可以内联getNum的所有实例,将num的值移动到寄存器中(或者简单地,注意到它从未被任何代码实际更改,将其视为常量)在程序的整个生命周期中。

Practically speaking, no (current) CPU has enough free registers that even the most agressive optimizing compiler would choose to do that. 实际上,没有(当前)CPU拥有足够的空闲寄存器,即使是最积极的优化编译器也会选择这样做。 But if correctness is desired - volatile is required. 但是,如果需要正确性,则需要挥发。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM