在 C/C++ 中原子按位与字节的最佳方法?

ven*_*ngy 1 c c++ atomic stdatomic

目前正在使用 GCC 查看 C/C++ 中的原子操作,发现内存中自然对齐的全局变量具有原子读取和写入。

然而,我试图按位与一个全局变量,并注意到它归结为一个读取-修改-写入序列,如果有多个线程对该字节值进行操作,那么这会很麻烦。

经过一番研究,我选择了这两个例子:

C 示例- GCC 扩展__sync_fetch_and_and

#include <stdio.h>
#include <stdint.h>

uint8_t byteC = 0xFF;

int main() {
    __sync_fetch_and_and(&byteC, 0xF0);
    printf("Value of byteC: 0x%X\n", byteC);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

C++ 示例- 使用原子的 C++11fetch_and

#include <iostream>
#include <atomic>

std::atomic<uint8_t> byteCpp(0xFF);

int main() {
    byteCpp.fetch_and(0xF0);
    std::cout << "Value of byteCpp: 0x" << std::hex << static_cast<int>(byteCpp.load()) << std::endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

其他示例如下,但它们似乎不太直观且计算成本更高。

用一个pthread_mutex_lock

uint8_t byte = 0xFF;
pthread_mutex_t byte_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&byte_mutex);
byte &= 0xF0;
pthread_mutex_unlock(&byte_mutex);
Run Code Online (Sandbox Code Playgroud)

使用互斥体lock_guard

#include <mutex>

uint8_t byte;
std::mutex byte_mutex;

void atomic_and() {
    std::lock_guard<std::mutex> lock(byte_mutex);
    byte &= 0xF0;
}
Run Code Online (Sandbox Code Playgroud)

用一个compare_exchange_weak

std::atomic<uint8_t> byte;

void atomic_and() {
    uint8_t old_val, new_val;
    do {
        old_val = byte.load();
        new_val = old_val & 0xF0;
    } while (!byte.compare_exchange_weak(old_val, new_val));
}
Run Code Online (Sandbox Code Playgroud)

问题

多线程 C/C++ 程序中读取-修改-写入序列的最佳原子方法是什么?

Jan*_*tke 7

[我]发现内存中自然对齐的全局变量具有原子读取和写入。

这在 C/C++ 意义上是不正确的,仅在 x86_64 意义上是正确的。确实,x86_64 上的任何对齐加载和存储都是原子的,但这对于抽象机来说是不正确的。同时写入内存的非原子位始终是一场数据竞争,线程清理程序可能会发现错误,即使该体系结构理论上使其安全。

此外,原子地执行的最佳方法byte &= 0xf0在 C 和 C++ 中非常相似:

// C++
#include <atomic>
std::atomic_uint8_t byte; // or std::atomic<std::uint8_t>
// ...
std::uint8_t old = byte.fetch_and(0xf0); /* optionally specify memory order */
// or
std::uint8_t old = std::atomic_fetch_and(&byte, 0xf0);
Run Code Online (Sandbox Code Playgroud)
// C (no compiler extensions/intrinsics needed)
#include <stdatomic.h>
atomic_uint8_t byte; // or _Atomic uint8_t
// ...
uint8_t old = atomic_fetch_and(&byte, 0xf0); /* optionally atomic_fetch_and_explicit */
Run Code Online (Sandbox Code Playgroud)

其他方法(POSIX 线程、std::mutexcompare_exchange试循环)几乎肯定比函数形式的内置方法更糟糕fetch_and。如果体系结构不直接提供原子获取与指令,则应选择最佳方式。这不是你需要担心的事情。


也可以看看

感谢@PeterCordes 分享这些链接。

  • 大多数 ISA 上的汇编都是如此,而不仅仅是 x86-64。这就是为什么 `std::atomic&lt;T&gt;` `.load(relaxed)/`.store(relaxed)` 只是编译为普通加​​载和存储指令,与编译器用于普通变量的指令相同,对于 T 来说是寄存器的宽度大多数 ISA 上或更少。但是,是的,完全有缺陷的推理,*在 C 或 C++* 中是原子的,也意味着不让优化器干预。请参阅 [LWN:谁害怕一个糟糕的优化编译器?](https://lwn.net/Articles/793253/) 回复:Linux 内核中普通变量的陷阱(其中他们将“易失性”与 GCC 和已知类型一起使用,和“asm”用于更强的排序)。 (2认同)
  • @vengy:那些人是超级错误的。如果您需要的只是原子性(并且让优化器知道另一次读取可能会获得新值),而不是排序,请将“std::atomic&lt;int&gt;”与“std::memory_order_relaxed”一起使用。任何其他操作。这就像 volatile int 给你的东西(用真正的编译器编译成大约相同的 asm),除了它是由 ISO C 和 ISO C++ 明确定义的。[何时在多线程中使用 volatile?](//stackoverflow.com/a/58535118)(除非您确切地知道自己在做什么,否则永远不要使用,例如对“atomic&lt;double&gt;”之类的东西进行微优化,其中 GCC 代码-一代很烂) (2认同)