现代CPU架构通常采用可导致无序执行的性能优化,这是很常见的.在单线程应用程序中,也可能发生内存重新排序,但它对程序员来说是不可见的,就好像按程序顺序访问内存一样.对于SMP来说,内存障碍可以用来强制执行某种内存排序.
我不确定的是关于单处理器中的多线程.请考虑以下示例:当线程1运行时,商店f可以在商店之前发生x.假设在f写入之后以及之前x写入上下文切换.现在线程2开始运行,它结束循环并打印0,这当然是不可取的.
// Both x, f are initialized w/ 0.
// Thread 1
x = 42;
f = 1;
// Thread 2
while (f == 0)
;
print x;
Run Code Online (Sandbox Code Playgroud)
上述情况可能吗?或者是否保证在线程上下文切换期间提交物理内存?
根据这个维基,
当程序在单CPU机器上运行时,硬件执行必要的簿记,以确保程序执行就好像所有内存操作都按程序员指定的顺序执行(程序顺序),因此不需要内存屏障.
虽然它没有明确提到单处理器多线程应用程序,但它包括这种情况.
我不确定它是否正确/完整.请注意,这可能高度依赖于硬件(弱/强内存模型).因此,您可能希望在答案中包含您知道的硬件.谢谢.
PS.设备I/O等不是我关心的问题.它是一个单核单处理器.
编辑:感谢Nitsan的提醒,我们假设这里没有编译器重新排序(只是硬件重新排序),并且线程2中的循环没有被优化掉.再次,魔鬼在细节中.
我已将其归结为一个简单的自包含示例.主线程将1000个项目排队,并且工作线程尝试同时出列.ThreadSanitizer抱怨在其中一个元素的读取和写入之间存在竞争,即使存在一个保护它们的获取释放内存屏障序列.
#include <atomic>
#include <thread>
#include <cassert>
struct FakeQueue
{
int items[1000];
std::atomic<int> m_enqueueIndex;
int m_dequeueIndex;
FakeQueue() : m_enqueueIndex(0), m_dequeueIndex(0) { }
void enqueue(int x)
{
auto tail = m_enqueueIndex.load(std::memory_order_relaxed);
items[tail] = x; // <- element written
m_enqueueIndex.store(tail + 1, std::memory_order_release);
}
bool try_dequeue(int& x)
{
auto tail = m_enqueueIndex.load(std::memory_order_acquire);
assert(tail >= m_dequeueIndex);
if (tail == m_dequeueIndex)
return false;
x = items[m_dequeueIndex]; // <- element read -- tsan says race!
++m_dequeueIndex;
return true;
}
};
FakeQueue q;
int main()
{ …Run Code Online (Sandbox Code Playgroud) 假设我有一个控制某个循环执行的字段:
private static bool shouldRun = true;
Run Code Online (Sandbox Code Playgroud)
我有一个运行的线程,其代码如下:
while(shouldRun)
{
// Do some work ....
Thread.MemoryBarrier();
}
Run Code Online (Sandbox Code Playgroud)
现在,另一个线程可能设置shouldRun为false,而不使用任何同步机制.
据我所知,Thread.MemoryBarrier(),在while循环中调用此函数将阻止我的工作线程获得缓存版本shouldRun,并有效防止无限循环发生.
我对Thread.MemoryBarrier的理解是否正确?鉴于我有可以设置shouldRun变量的线程(这不容易改变),这是一种合理的方法来确保我的循环一旦shouldRun被任何线程设置为false 就会停止吗?
我的理解std::memory_order_acquire和std::memory_order_release如下:
获取意味着获取围栏之后出现的内存访问不能重新排序到围栏之前.
释放意味着在释放围栏之前出现的内存访问不能在围栏之后重新排序.
我不明白为什么特别是对于C++ 11原子库,获取围栏与加载操作相关联,而释放围栏与存储操作相关联.
为了澄清,C++ 11 <atomic>库允许您以两种方式指定内存屏障:要么可以将fence指定为原子操作的额外参数,例如:
x.load(std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
或者您可以std::memory_order_relaxed单独使用和指定围栅,例如:
x.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
我不明白的是,鉴于上述获取和发布的定义,为什么C++ 11特别将获取与加载相关联,并与商店一起发布?是的,我已经看过许多示例,这些示例显示了如何使用获取/加载与发布/存储来在线程之间进行同步,但一般来说似乎是获取fences(防止语句后的内存重新排序)和发布的想法fences(在语句之前防止内存重新排序)与加载和存储的想法正交.
那么,为什么,例如,编译器不会让我说:
x.store(10, std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)
我意识到我可以通过使用memory_order_relaxed,然后单独的atomic_thread_fence(memory_order_acquire)声明来完成上述操作,但同样,为什么我不能直接使用存储memory_order_acquire?
一个可能的用例可能是,如果我想确保某些存储,比如在执行可能影响其他线程的其他语句之前x = 10发生.
我试图准确理解什么是内存障碍.根据我目前所知,存储器屏障(例如:) mfence用于防止指令从存储器屏障之前到之后和之后重新排序.
这是使用中的内存屏障的示例:
instruction 1
instruction 2
instruction 3
mfence
instruction 4
instruction 5
instruction 6
Run Code Online (Sandbox Code Playgroud)
现在我的问题是:mfence指令只是一个标记,告诉CPU执行指令的顺序是什么?或者它是CPU实际执行的指令,就像它执行其他指令(例如:) mov.
我目前正在阅读Anthony Williams的C++ Concurrency in Action.他的一个列表显示了这段代码,他声明z != 0可以解雇的断言.
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{
x.store(true,std::memory_order_release);
}
void write_y()
{
y.store(true,std::memory_order_release);
}
void read_x_then_y()
{
while(!x.load(std::memory_order_acquire));
if(y.load(std::memory_order_acquire))
++z;
}
void read_y_then_x()
{
while(!y.load(std::memory_order_acquire));
if(x.load(std::memory_order_acquire))
++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load()!=0);
}
Run Code Online (Sandbox Code Playgroud)
所以我能想到的不同执行路径是这样的:
1)
Run Code Online (Sandbox Code Playgroud)Thread a (x is now true) Thread c (fails to increment z) Thread b (y …
以前我写过一些非常简单的多线程代码,而且我一直都知道在任何时候都可以在我正在做的事情中间进行上下文切换,所以我总是通过以下方式保护访问共享变量一个CCriticalSection类,它进入构造的关键部分并使其破坏.我知道这是相当激进的,我进入和离开关键部分非常频繁,有时非常惊人(例如,当我可以将CCriticalSection置于更严格的代码块中时,在函数的开头)但我的代码没有崩溃并且运行得足够快.
在工作中,我的多线程代码需要更紧密,只需要在最低级别锁定/同步.
在工作中我试图调试一些多线程代码,我遇到了这个:
EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);
Run Code Online (Sandbox Code Playgroud)
现在,m_bSomeVariable是Win32 BOOL(非易失性),据我所知,它被定义为一个int,并且在x86上读取和写入这些值是一条指令,并且因为上下文切换发生在指令边界上,所以没有必要用于将此操作与关键部分同步.
我在网上做了一些更多的研究,看看这个操作是否不需要同步,我想出了两个场景:
我相信使用"volatile"关键字可以解决数字1.在VS2005及更高版本中,C++编译器使用内存屏障围绕对此变量的访问,确保在使用变量之前始终将变量完全写入/读取到主系统内存.
2号我无法验证,我不知道为什么字节对齐会产生影响.我不知道x86指令集,但是mov需要给出一个4字节对齐的地址吗?如果不是,您需要使用指令组合吗?这会引入问题.
所以...
问题1:使用"volatile"关键字(隐含使用内存屏障并暗示编译器不优化此代码)可以避免程序员在读取/读取x86/x64变量之间同步4字节/ 8字节的需要写操作?
问题2:是否明确要求变量为4字节/ 8字节对齐?
我做了一些深入研究我们的代码和类中定义的变量:
class CExample
{
private:
CRITICAL_SECTION m_Crit1; // Protects variable a
CRITICAL_SECTION m_Crit2; // Protects variable b
CRITICAL_SECTION m_Crit3; // Protects variable c
CRITICAL_SECTION m_Crit4; // Protects variable d
// ...
};
Run Code Online (Sandbox Code Playgroud)
现在,对我来说这似乎过分了.我认为关键部分在一个进程之间同步线程,所以如果你有一个,你可以输入它,并且该进程中没有其他线程可以执行.对于要保护的每个变量,不需要关键部分,如果您处于关键部分,那么没有其他任何内容可以打断您.
我认为唯一可以从关键部分外部更改变量的是,如果进程与另一个进程共享一个内存页面(你可以这样做吗?),另一个进程开始更改值.互斥体在这里也有帮助,命名互斥体是跨进程共享的,还是只有同名的进程共享?
问题3:我对关键部分的分析是否正确,是否应该重写此代码以使用互斥锁?我看过其他同步对象(信号量和自旋锁),它们更适合这里吗?
问题4:关键部分/互斥体/信号量/自旋锁哪里最适合?也就是说,它们应该应用于哪个同步问题.选择一个而不是另一个会有很大的性能损失吗?
虽然我们正在研究它,但我读到自旋锁不应该用在单核多线程环境中,只能用于多核多线程环境.所以,问题5:这是错的,或者如果不是,为什么是对的?
在此先感谢任何回复:)
8.1.2总线锁定
Intel 64和IA-32处理器提供LOCK#信号,该信号在某些关键存储器操作期间自动置位,以锁定系统总线或等效链路.当该输出信号被断言时,来自其他处理器或总线代理的用于控制总线的请求被阻止.软件可以指定在遵循LOCK语义的其他情况下将LOCK前缀添加到指令之前.
它来自英特尔手册,第3卷
听起来内存上的原子操作将直接在内存(RAM)上执行.我很困惑,因为当我分析装配输出时,我看到"没什么特别的".基本上,生成的汇编输出std::atomic<int> X; X.load()只会产生"额外"的影响.但是,它负责正确的内存排序,而不是原子性.如果我理解得X.store(2)恰到好处mov [somewhere], $2.就这样.它似乎没有"跳过"缓存.我知道将对齐(例如int)移动到内存是原子的.但是,我很困惑.
所以,我提出了疑问,但主要问题是:
我在以下代码中有关于操作顺序的问题:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_relaxed);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
鉴于std::memory_order_acquirecppreference页面上的描述(https://en.cppreference.com/w/cpp/atomic/memory_order),
具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的读取或写入进行重新排序.
很明显,r1 == 0 && r2 == 0在跑步thread1和thread2同时之后永远不会有结果.
但是,我在C++标准中找不到任何措辞(现在查看C++ 14草案),这保证了两个宽松的加载不能与获取 - 释放交换重新排序.我错过了什么?
编辑:正如评论中所建议的那样,实际上可以使r1和r2都等于零.我已经更新了程序以使用load-acquire,如下所示:
std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
y.exchange(1, std::memory_order_acq_rel);
r1 = x.load(std::memory_order_acquire);
}
void thread2() {
x.exchange(1, std::memory_order_acq_rel);
r2 = y.load(std::memory_order_acquire);
}
Run Code Online (Sandbox Code Playgroud)
现在是有可能得到两个和r1以及 …
在内核端很容易设置内存障碍:由于Linux内核头文件,宏mb,wmb,rmb等总是存在.
如何在用户端完成此操作?