如果有两个线程访问全局变量,那么许多教程都说使变量volatile变为阻止编译器将变量缓存在寄存器中,从而无法正确更新.但是,访问共享变量的两个线程是通过互斥锁来调用保护的东西不是吗?但是在这种情况下,在线程锁定和释放互斥锁之间,代码处于一个关键部分,只有那个线程可以访问变量,在这种情况下变量不需要是volatile?
那么多线程程序中volatile的用途/目的是什么?
编译器或操作系统如何区分sig_atomic_t类型和普通的int类型变量,并确保操作是原子的?使用两者的程序具有相同的汇编代码.如何特别注意使操作成为原子?
假设我有两个线程来操纵全局变量x.每个线程(或我认为的每个核心)都有一个缓存副本x.
现在说Thread A执行以下说明:
set x to 5
some other instruction
Run Code Online (Sandbox Code Playgroud)
现在set x to 5执行时,缓存的值x将设置为5,这将导致缓存一致性协议使用新值来操作和更新其他核心的缓存x.
现在我的问题是:什么时候x实际设置5在Thread A缓存中,其他内核的缓存在some other instruction执行之前是否会更新?或者应该使用内存屏障来确保?:
set x to 5
memory barrier
some other instruction
Run Code Online (Sandbox Code Playgroud)
注意:假设指令是按顺序执行的,也假设set x to 5执行时,5会立即放入线程A的缓存中(因此指令不会放在队列中或稍后要执行的内容).
Herb Sutter 在他的“原子<>武器”演讲中展示了原子的几个示例用途,其中之一可归结为以下内容:(视频链接,带时间戳)
一个主线程启动多个工作线程。
工人检查停止标志:
while (!stop.load(std::memory_order_relaxed))
{
// Do stuff.
}
Run Code Online (Sandbox Code Playgroud)
主线程最终执行此操作stop = true;(注意,使用 order= seq_cst),然后加入工作线程。
Sutter 解释说,使用 order= 检查标志relaxed是可以的,因为谁在乎线程是否会因稍大的延迟而停止。
但为什么要stop = true;在主线程中使用呢seq_cst?幻灯片上说这是故意不这样做relaxed,但没有解释原因。
看起来它会起作用,可能会有更大的停止延迟。
这是性能和其他线程看到标志的速度之间的折衷吗?即,由于主线程仅设置标志一次,我们不妨使用最强的排序,以尽快传达消息?
据我所知,编译器从不优化声明为的变量volatile.但是,我有一个像这样声明的数组.
volatile long array[8];
Run Code Online (Sandbox Code Playgroud)
不同的线程读写它.数组的元素仅由其中一个线程修改,并由任何其他线程读取.但是,在某些情况下,我注意到即使我从一个线程修改一个元素,读取它的线程也不会注意到这个变化.它继续读取相同的旧值,就好像编译器已将其缓存在某处.但是编译器本身不应该缓存volatile变量,对吧?那怎么会发生这种情况.
注意:我不是volatile用于线程同步,所以请停止给我答案,如使用锁或原子变量.我知道volatile,atomic变量和互斥量之间的区别.另请注意,该体系结构是x86,具有主动缓存一致性.在我认为变量被其他线程修改后,我也读了很长时间.即使经过很长一段时间,阅读线程也看不到修改后的值.
我知道如何使用LOCK线程安全地增加一个值:
lock inc [J];
Run Code Online (Sandbox Code Playgroud)
但是我如何以线程安全的方式阅读[J](或任何值)?LOCK前缀不能与mov一起使用.如果我做以下事情:
xor eax, eax;
lock add eax, [J];
mov [JC], eax;
Run Code Online (Sandbox Code Playgroud)
它在第2行引发错误.
我已经阅读了很多关于内存排序的文章,并且所有这些文章都只说CPU重新加载和存储.
CPU(我对x86 CPU特别感兴趣)是否仅重新排序加载和存储,并且不重新排序它具有的其余指令?
据我了解,当 CPU 推测性地执行一段代码时,它会在切换到推测性分支之前“备份”寄存器状态,以便如果预测结果错误(使分支无用)——寄存器状态将是安全恢复,而不会破坏“状态”。
所以,我的问题是:推测执行的 CPU 分支是否可以包含访问 RAM 的操作码?
我的意思是,访问 RAM 不是“原子”操作——如果数据当前不在 CPU 缓存中,那么从内存中读取一个简单的操作码可能会导致实际的 RAM 访问,这可能会变成一个非常耗时的操作,从 CPU 的角度来看。
如果在推测分支中确实允许这种访问,它是否仅用于读取操作?因为,我只能假设,如果一个分支被丢弃并执行“回滚”,根据它的大小恢复写操作可能会变得非常缓慢和棘手。而且,可以肯定的是,至少在某种程度上支持读/写操作,因为寄存器本身,在某些 CPU 上,据我所知,物理上位于 CPU 缓存上。
所以,也许更精确的表述是:推测执行的一段代码有什么限制?
由于存储负载转发,某些负载指令能否在全局范围内不可见?换句话说,如果加载指令从存储缓冲区中获取其值,则它永远不必从高速缓存中读取。
通常说来,当从L1D缓存读取负载时,该负载在全局范围内可见,因此,未从L1D读取的负载应使其在全局上不可见。
我读过有关内存屏障如何工作的不同内容。
例如,用户Johan在这个问题中的回答说,内存屏障是 CPU 执行的指令。
虽然用户Peter Cordes在这个问题中的评论说了以下关于 CPU 如何重新排序指令的内容:
它的读取速度比执行速度快,因此它可以看到即将到来的指令的窗口。有关详细信息,请参阅 x86 标签 wiki 中的一些链接,例如 Agner Fog 的 microarch pdf,以及 David Kanter 对 Intel Haswell 设计的文章。当然,如果您只是用谷歌搜索“乱序执行”,您会找到您应该阅读的维基百科文章。
所以我根据上面的评论猜测,如果指令之间存在内存屏障,CPU将看到这个内存屏障,这导致CPU不会对指令重新排序,所以这意味着内存屏障是一个“标记”让CPU看到而不是执行。
现在我的猜测是,内存屏障既充当标记又充当 CPU 执行的指令。
对于标记部分,CPU 看到指令之间存在内存屏障,这导致 CPU 不会对指令进行重新排序。
至于指令部分,CPU会执行内存屏障指令,这会导致CPU做一些诸如刷新存储缓冲区之类的事情,然后CPU会继续执行内存屏障之后的指令。
我对么?
x86 assembly instruction-set cpu-architecture memory-barriers
TL;DR:在生产者-消费者队列中,放置一个不必要的(从 C++ 内存模型的角度来看)内存栅栏或不必要的强内存顺序是否有必要以牺牲可能更差的吞吐量为代价来获得更好的延迟?
C++ 内存模型是在硬件上执行的,方法是使用某种内存栅栏来实现更强的内存顺序,而不是将它们放在较弱的内存顺序上。
特别是,如果生产者这样做store(memory_order_release),而消费者使用 观察存储的值load(memory_order_acquire),则加载和存储之间没有围栏。在 x86 上根本没有栅栏,在 ARM 上栅栏是在存储之前和加载之后进行放置操作。
没有围栏存储的值最终会被没有围栏的负载观察到(可能在几次不成功的尝试之后)
我想知道在队列的两侧放置围栏是否可以更快地观察到值?如果有围栏和没有围栏,延迟是多少?
我希望只有一个循环load(memory_order_acquire)和pause/yield限制为数千次迭代是最好的选择,因为它无处不在,但想了解原因。
由于这个问题是关于硬件行为的,我希望没有通用的答案。如果是这样,我主要想知道 x86(x64 风格),其次是 ARM。
例子:
T queue[MAX_SIZE]
std::atomic<std::size_t> shared_producer_index;
void producer()
{
std::size_t private_producer_index = 0;
for(;;)
{
private_producer_index++; // Handling rollover and queue full omitted
/* fill data */;
shared_producer_index.store(
private_producer_index, std::memory_order_release);
// Maybe barrier here or stronger order above?
}
}
void consumer()
{
std::size_t private_consumer_index = 0;
for(;;)
{
std::size_t observed_producer_index = shared_producer_index.load( …Run Code Online (Sandbox Code Playgroud) 参考下面的代码
auto x = std::atomic<std::uint64_t>{0};
auto y = std::atomic<std::uint64_t>{0};
// thread 1
x.store(1, std::memory_order_release);
auto one = y.load(std::memory_order_seq_cst);
// thread 2
y.fetch_add(1, std::memory_order_seq_cst);
auto two = x.load(std::memory_order_seq_cst);
Run Code Online (Sandbox Code Playgroud)
这里有可能one和two都为 0 吗?
(我似乎遇到了一个错误,在上面的代码运行后,如果one和two都可以保持 0 的值,则可以解释该错误。并且排序规则太复杂,我无法弄清楚上面可以进行哪些排序。)
考虑两个线程,T1 和 T2,它们分别存储和加载一个原子整数 a_i。让我们进一步假设,这家店在执行前正在执行的负荷启动。以前,我的意思是绝对的时间意义。
T1 T2
// other_instructions here... // ...
a_i.store(7, memory_order_relaxed) // other instructions here
// other instructions here // ...
a_i.load(memory_order_relaxed)
// other instructions here
Run Code Online (Sandbox Code Playgroud)
是否保证T2在加载后看到值7?