在多核CPU上读取32位的原子性

Wad*_*Wad 6 c++ winapi multithreading atomic

(注意:我已经根据我认为人们可能会提供帮助的地方添加了这个问题的标签,所以请不要大喊:))

在我的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
Run Code Online (Sandbox Code Playgroud)

现在考虑这个代码,在线程#2上执行:

if (m_lClosed) // Do something
Run Code Online (Sandbox Code Playgroud)

我理解在单个CPU上,这不会是一个问题,因为更新是原子的,读取也是原子的(参见MSDN),因此线程抢占不能使变量处于部分更新状态.但是在多核CPU上,如果每个线程都在不同的CPU上,我们真的可以让这两段代码并行执行.在这个例子中,我认为这不会是一个问题,但是在测试可能正在更新的过程中仍然感觉不对.

这个网页告诉我,多个CPU的原子性是通过LOCK汇编指令实现的,防止其他CPU访问该内存.这听起来像我需要的,但上面为if测试生成的汇编语言仅仅是

cmp   dword ptr [l],0  
Run Code Online (Sandbox Code Playgroud)

......没有任何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;
};
Run Code Online (Sandbox Code Playgroud)

来电者会打电话SetClosed(); 如果它返回true,它将调用::closesocket()等.请不要质疑为什么它是这样,它只是:)

考虑如果一个线程关闭套接字而另一个线程尝试发布套接字会发生什么WSARecv().你可能认为WSARecv()会失败(套接字毕竟是关闭的!); 但是,如果使用与我们刚关闭的套接字句柄相同的套接字句柄建立新连接,那么我们将发布WSARecv()哪个会成功,但这对我的程序逻辑来说是致命的,因为我们现在正在关联一个完全不同的连接CConnection对象.因此,我有if (!m_lClosed)测试.您可能会争辩说我不应该在多个线程中处理相同的连接,但这不是这个问题的重点 :)

这就是我打电话m_lClosed之前需要测试的原因WSARecv().

现在,显然,我只是设置m_lClosed为1,所以一个撕裂的读/写并不是真正的问题,但这是我关注的原则.如果我设置m_lClosed为2147483647然后测试2147483647怎么办?在这种情况下,撕裂的读/写将更成问题.

And*_*ers 10

这实际上取决于您的编译器和运行的CPU.

LOCK如果内存地址正确对齐,x86 CPU将自动读取没有前缀的32位值.但是,如果将变量用作某些其他相关数据的锁定/计数,则很可能需要某种内存屏障来控制CPU的无序执行.未对齐的数据可能无法以原子方式读取,尤其是当值跨越页面边界时.

如果您不是手动编码程序集,则还需要担心编译器重新排序优化.

标记为的任何变量volatile使用Visual C++编译时都会在编译器(以及可能生成的机器代码)中具有排序约束:

_ReadBarrier,_WriteBarrier和_ReadWriteBarrier编译器内在函数仅阻止编译器重新排序.使用Visual Studio 2003,可以订购易失性到易失性的引用; 编译器不会重新命令volatile变量访问.使用Visual Studio 2005,编译器还使用获取语义对volatile变量进行读操作,并为volatile变量上的写操作释放语义(当CPU支持时).

Microsoft特定的volatile关键字增强功能:

当使用/ 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原子操作最有可能最终成为MOVx86上的简单指令.LOCK如果编译器确定目标平台需要它,则更严格的内存顺序可以添加围栏和可能的前缀.

  • OP在`VS2017`上,所以我们至少应该提到`<atomic>`.易失性与原子无关,除了在MSVC上.但是,即使在MSVC上,MS本身[强烈建议](https://msdn.microsoft.com/en-us/library/jj204392.aspx)使用`volatile:iso`,即使用标准语义来表示`volatile `(没有记忆障碍). (2认同)

Wad*_*Wad -1

好吧,事实证明这确实没有必要;这个答案详细解释了为什么我们不需要使用任何互锁操作来进行简单的读/写(但我们需要使用读-修改-写)。