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

Atomicity of 32bit read on multicore CPU

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

在我的 VS 2017 64 位项目中,我有一个 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 上,我们真的可以让这两段代码并行执行。在这个例子中,我认为这不是问题,但测试可能正在更新的东西仍然感觉不对。

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

cmp   dword ptr [l],0  

...看不到 LOCK 指令。

在这种情况下,我们应该如何确保读取的原子性?

编辑 24/4/18

首先感谢大家对这个问题的兴趣。我在下面显示实际代码;我故意保持简单,专注于这一切的原子性,但显然,如果我从第一分钟开始就展示一切会更好。

其次,实际代码所在的项目是VS2005项目; 因此无法访问 C++11 原子。这就是为什么我没有在问题中添加 C++11 标签的原因。我将 VS2017 与 "scratch" 项目一起使用,以节省每次我在学习期间进行更改时都必须构建巨大的 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();如果 returns 为真,它将调用 ::closesocket() 等。请不要质疑为什么会这样,它就是:)

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

这就是为什么我需要在调用 WSARecv() 之前测试 m_lClosed

现在,很明显,我只是将 m_lClosed 设置为 1,所以撕裂 read/write 并不是真正的问题,但 这是我关心的原则。如果我将 m_lClosed 设置为 2147483647 然后测试 2147483647 会怎样?在这种情况下,撕裂 read/write 会更成问题。

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

如果内存地址正确对齐,

x86 CPUs 将自动读取不带 LOCK 前缀的 32 位值。但是,如果变量用作某些其他相关数据的 lock/count,您很可能需要某种 memory barrier 来控制 CPU 和 out-of-order 的执行。未对齐的数据可能无法自动读取,尤其是当值跨越页面边界时。

如果您不是手工编码汇编,您还需要担心 compilers reordering optimizations

compiling with Visual C++:

时,任何标记为 volatile 的变量在编译器(以及可能生成的机器代码)中将具有顺序约束

The _ReadBarrier, _WriteBarrier, and _ReadWriteBarrier compiler intrinsics prevent compiler re-ordering only. With Visual Studio 2003, volatile to volatile references are ordered; the compiler will not re-order volatile variable access. With Visual Studio 2005, the compiler also uses acquire semantics for read operations on volatile variables and release semantics for write operations on volatile variables (when supported by the CPU).

Microsoft specific volatile keyword enhancements:

When the /volatile:ms compiler option is used—by default when architectures other than ARM are targeted—the compiler generates extra code to maintain ordering among references to volatile objects in addition to maintaining ordering to references to other global objects. In particular:

  • A write to a volatile object (also known as volatile write) has Release semantics; that is, a reference to a global or static object that occurs before a write to a volatile object in the instruction sequence will occur before that volatile write in the compiled binary.

  • A read of a volatile object (also known as volatile read) has Acquire semantics; that is, a reference to a global or static object that occurs after a read of volatile memory in the instruction sequence will occur after that volatile read in the compiled binary.

This enables volatile objects to be used for memory locks and releases in multithreaded applications.


For architectures other than ARM, if no /volatile compiler option is specified, the compiler performs as if /volatile:ms were specified; therefore, for architectures other than ARM we strongly recommend that you specify /volatile:iso, and use explicit synchronization primitives and compiler intrinsics when you are dealing with memory that is shared across threads.

Microsoft 为大多数 Interlocked* 函数提供了 compiler intrinsics,它们将编译成类似 LOCK XADD ... 的东西,而不是函数调用。

直到“最近”,C/C++ 通常不支持原子操作或线程,但在添加了原子支持的 C11/C++11 中改变了这一点。使用 <atomic> header 及其 types/functions/classes 将对齐和重新排序的责任移交给编译器,因此您不必担心。您仍然必须就内存屏障做出选择,这决定了编译器生成的机器代码。在宽松的内存顺序下,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()) { /* ... */ }

这绰绰有余(它强制顺序一致性,这可能很昂贵)。在某些情况下,可以通过放宽原子操作的内存顺序来生成稍微更快的代码,但我不担心。

正如我发布的 , this question was never about protecting a critical section of code, it was purely about avoiding torn read/writes. user3386109 posted a comment which I ended up using, but declined posting it as an answer 。因此,我提供了最终用于完成此问题的解决方案;也许它会在将来帮助某人。

下面是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!";

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