![](/img/trans.png)
[英]How does 64bit integer type guarantee atomicity on 32bit operation system?
[英]Atomicity of 32bit read on multicore CPU
(注意:我已经根据我认为人们可能会提供帮助的地方添加了这个问题的标签,所以请不要大喊:))
在我的VS 2017 64bit项目中,我有一个32位长的值m_lClosed
。 当我想更新它时,我使用Interlocked
系列函数之一。
考虑这个代码,在线程#1上执行
LONG lRet = InterlockedCompareExchange(&m_lClosed, 1, 0); // Set m_lClosed to 1 provided it's currently 0
现在考虑这个代码,在线程#2上执行:
if (m_lClosed) // Do something
我理解在单个CPU上,这不会是一个问题,因为更新是原子的,读取也是原子的(参见MSDN ),因此线程抢占不能使变量处于部分更新状态。 但是在多核CPU上,如果每个线程都在不同的CPU上,我们真的可以让这两段代码并行执行。 在这个例子中,我认为这不会是一个问题,但是在测试可能正在更新的过程中仍然感觉不对。
该网页告诉我,多个CPU的原子性是通过LOCK
汇编指令实现的,防止其他CPU访问该内存。 这听起来像我需要的,但上面为if测试生成的汇编语言仅仅是
cmp dword ptr [l],0
......看不到LOCK
指令。
在这样的事件中,我们应该如何确保读取的原子性?
编辑24/4/18
首先感谢这个问题产生的所有兴趣。 我在下面显示实际代码; 我故意把它简单地集中在它的所有原子性上,但显然如果我从一分钟那里展示它就会更好。
其次,实际代码所在的项目是VS2005项目; 因此无法访问C ++ 11原子 。 这就是我没有在问题中添加C ++ 11标签的原因。 我正在使用VS2017进行“刮擦”项目,以便在我学习的时候每次做出改变时都要建立一个巨大的VS2005。 另外,它是一个更好的IDE。
是的,所以实际代码存在于IOCP驱动的服务器中,这整个原子性是关于处理一个封闭的套接字:
class CConnection
{
//...
DWORD PostWSARecv()
{
if (!m_lClosed)
return ::WSARecv(...);
else
return WSAESHUTDOWN;
}
bool SetClosed()
{
LONG lRet = InterlockedCompareExchange(&m_lClosed, 1, 0); // Set m_lClosed to 1 provided it's currently 0
// If the swap was carried out, the return value is the old value of m_lClosed, which should be 0.
return lRet == 0;
}
SOCKET m_sock;
LONG m_lClosed;
};
调用者将调用SetClosed()
; 如果它返回true,它将调用::closesocket()
等。请不要问为什么它是这样的,它只是:)
考虑如果一个线程关闭套接字而另一个线程试图发布WSARecv()
会发生什么。 你可能认为WSARecv()
会失败(套接字毕竟是关闭的!); 但是,如果使用与我们刚刚关闭的套接字句柄相同的套接字句柄建立新连接,那么我们将发布成功的WSARecv()
,但这对我的程序逻辑来说是致命的,因为我们现在正在关联一个完全不同的与此CConnection对象的连接。 因此,我有if (!m_lClosed)
测试。 您可能会争辩说我不应该在多个线程中处理相同的连接, 但这不是这个问题的重点 :)
这就是为什么我需要在进行WSARecv()
调用之前测试m_lClosed
。
现在,显然,我只是将m_lClosed
设置为1,所以一个撕裂的读/写并不是真正的问题,但这是我关注的原则 。 如果我将m_lClosed
设置为2147483647然后测试2147483647怎么办? 在这种情况下,撕裂的读/写将更成问题。
这实际上取决于您的编译器和运行的CPU。
如果内存地址正确对齐,x86 CPU将自动读取没有LOCK
前缀的32位值。 但是,如果将变量用作某些其他相关数据的锁定/计数,则很可能需要某种内存屏障来控制CPU的无序执行。 未对齐的数据可能无法以原子方式读取,尤其是当值跨越页面边界时。
如果您不是手动编码程序集,则还需要担心编译器重新排序优化 。
标记为volatile
任何变量在使用Visual C ++编译时都会在编译器(以及可能生成的机器代码)中具有排序约束:
_ReadBarrier,_WriteBarrier和_ReadWriteBarrier编译器内在函数仅阻止编译器重新排序。 使用Visual Studio 2003,可以订购易失性到易失性的引用; 编译器不会重新命令volatile变量访问。 使用Visual Studio 2005,编译器还使用获取语义对volatile变量进行读操作,并为volatile变量上的写操作释放语义(当CPU支持时)。
当使用/ volatile:ms编译器选项时 - 默认情况下,当ARM以外的体系结构成为目标时 - 除了维护对其他全局对象的引用的排序之外,编译器还会生成额外的代码来维护对volatile对象的引用之间的排序。 尤其是:
对volatile对象的写入(也称为volatile write)具有Release语义; 也就是说,在写入指令序列中的易失性对象之前发生的对全局或静态对象的引用将在编译二进制文件中的易失性写入之前发生。
读取volatile对象(也称为volatile读取)具有Acquire语义; 也就是说,在读取指令序列中的易失性存储器之后发生的对全局或静态对象的引用将在编译二进制文件中的易失性读取之后发生。
这使得volatile对象可用于多线程应用程序中的内存锁定和释放。
对于ARM以外的体系结构,如果未指定/ volatile编译器选项,则编译器将执行,如同指定/ volatile:ms; 因此,对于ARM以外的体系结构,我们强烈建议您指定/ volatile:iso,并在处理跨线程共享的内存时使用显式同步原语和编译器内在函数。
Microsoft为大多数Interlocked *函数提供编译器内在函数,它们将编译为类似LOCK XADD ...
而不是函数调用。
直到“最近”,C / C ++一般不支持原子操作或线程,但在C11 / C ++ 11中,这已经改变了原子支持。 使用<atomic>
头及其类型/函数/类将对齐和重新排序的责任移动到编译器,因此您不必担心这一点。 您仍然需要对内存障碍做出选择,这决定了编译器生成的机器代码。 随着内存顺序的放松, load
原子操作最有可能最终成为x86上的简单MOV
指令。 如果编译器确定目标平台需要它,则更严格的内存顺序可以添加栅栏和可能的LOCK
前缀。
在C ++ 11中,对非原子对象(例如m_lClosed
)的非同步访问是未定义的行为。
该标准提供了正确写入所需的所有设施; 您不需要InterlockedCompareExchange
等非便携式功能。 相反,只需将变量定义为atomic
:
std::atomic<bool> m_lClosed{false};
// Writer thread...
bool expected = false;
m_lClosed.compare_exhange_strong(expected, true);
// Reader...
if (m_lClosed.load()) { /* ... */ }
这绰绰有余(它强制顺序一致,这可能很昂贵)。 在某些情况下,可以通过放松原子操作的内存顺序来生成稍快的代码,但我不担心。
正如我在这里发布的那样,这个问题从未涉及保护代码的关键部分,它纯粹是为了避免破坏读/写。 user3386109在这里发表评论,我最终使用了,但拒绝将其作为答案发布在这里 。 因此,我提供了最终用于完成此问题的解决方案; 也许它会在将来帮助某人。
以下显示了m_lClosed
的原子设置和测试:
long m_lClosed = 0;
线程1
// Set flag to closed
if (InterlockedCompareExchange(&m_lClosed, 1, 0) == 0)
cout << "Closed OK!\n";
线程2
此代码替换if (!m_lClosed)
if (InterlockedCompareExchange(&m_lClosed, 0, 0) == 0)
cout << "Not closed!";
好的,事实证明这确实没有必要; 这个答案详细解释了为什么我们不需要使用任何互锁操作来进行简单的读/写操作(但是我们做了读 - 修改 - 写)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.