gpt4 book ai didi

c++ - std::atomic 与另一个字符 union

转载 作者:可可西里 更新时间:2023-11-01 18:27:51 34 4
gpt4 key购买 nike

我最近阅读了一些在同一个 union 中具有原子和字符的代码。像这样的东西

union U {
std::atomic<char> atomic;
char character;
};

我不完全确定这里的规则,但代码注释说,由于一个字符可以给任何东西起别名,如果我们保证不更改字节的最后几位,我们就可以安全地对原子变量进行操作。并且该字符仅使用最后几个字节。

这是允许的吗?我们可以在一个字符上覆盖一个原子整数并使它们都处于事件状态吗?如果是这样,当一个线程尝试从原子整数加载值而另一个线程写入字符(仅最后几个字节)时会发生什么,字符写入是否是原子写入?那里会发生什么?是否必须为尝试加载原子整数的线程刷新缓存?

(这段代码在我看来也很臭,我不提倡使用它。只是想了解可以定义上述方案的哪些部分以及在什么情况下)

根据要求,代码正在做这样的事情
// thread using the atomic
while (!atomic.compare_exchange_weak(old_value, old_value | mask, ...) { ... }

// thread using the character
character |= 0b1; // set the 1st bit or something

最佳答案

the code comments said that since a character can alias anything, we can safely operate on the atomic variable if we promise not to change the last few bits of the byte.



这些评论是错误的。 char -can-alias-anything 并不能阻止它成为非原子变量的数据竞争,所以理论上是不允许的,更糟糕​​的是, 当由任何普通编译器(如 gcc、clang 或 MSVC)为任何普通 CPU(如 x86)编译时,它实际上已损坏。

原子性的单位是内存位置,而不是内存位置内的位。 ISO C++11 标准 defines "memory location" carefully ,所以 char[] 中的相邻元素数组或结构是单独的位置(因此 it's not a race if two threads write c[0] and c[1] without synchronization )。但是结构体中相邻的位域不是单独的内存位置,并且使用 |=在非原子 char别名为与 atomic<char> 相同的地址绝对是相同的内存位置,无论在 |= 的右侧设置了哪些位.

对于没有数据争用 UB 的程序,如果某个内存位置是由任何线程写入的,则所有其他线程(可能)同时访问该内存位置的所有其他线程都必须使用原子操作来执行此操作。 (也可能通过完全相同的对象,即通过类型双关将 atomic<int> 的中间字节更改为 atomic<char> 也不能保证是安全的。在类似于“普通”现代 CPU 的硬件上的大多数实现中,如果 atomic 都是无锁的,则类型对不同的 atomic<int/char> 类型的类型可能仍然是原子的,但内存排序语义实际上可能会被破坏,特别是如果它不是完全重叠的。

此外,通常在 ISO C++ 中不允许 union 类型双关。我认为您实际上需要将指针转换为 char* , 不与 char union . union 类型双关在 ISO C99 中是允许的,在 GNU C89 和 GNU C++ 中作为 GNU 扩展,也在一些其他 C++ 实现中。

这样就解决了理论问题,但是这些对当前的 CPU 是否有效? 不,它在实践中也是完全不安全的 .
character |= 1将(在普通计算机上)编译为加载整个 char 的 asm , 修改临时值,然后将值存储回来。在 x86 上,这一切都可能发生在一个内存目的地 or指令,如果编译器选择这样做(如果它稍后也需要该值,则不会这样做)。但即便如此,它仍然是一个非原子的 RMW,可以对其他位进行修改。

原子性对于读-修改-写操作来说是昂贵且可选的,并且在一个字节中设置某些位而不影响其他位的唯一方法是在当前 CPU 上进行读-修改-写。如果您特别要求,编译器只会发出以原子方式执行的 asm。 (与纯存储或纯负载不同,它们通常是自然原子的。 But always use std::atomic to get the other semantics you want...)

考虑以下事件序列:
 thread A           |     thread B
-------------------|--------------
read tmp=c=0000 |
|
| c|=0b1100 # atomically, leaving c = 1100
tmp |= 1 # tmp=1 |
store c = tmp

离开 c = 1,不是 1101你希望。即高位的非原子加载/存储踩到了线程 B 的修改。

我们得到的 asm 可以做到这一点 ,从问题( on the Godbolt compiler explorer )中编译源代码片段:
void t1(U &v, unsigned mask) {
// thread using the atomic
char old_value = v.atomic.load(std::memory_order_relaxed);
// with memory_order_seq_cst as the default for CAS
while (!v.atomic.compare_exchange_weak(old_value, old_value | mask)) {}

// v.atomic |= mask; // would have been easier and more efficient than CAS
}

t1(U&, unsigned int):
movzx eax, BYTE PTR [rdi] # atomic load of the old value
.L2:
mov edx, eax
or edx, esi # esi = mask (register arg)
lock cmpxchg BYTE PTR [rdi], dl # atomic CAS, uses AL implicitly as the expected value, same semantics as C++11 comp_exg seq_cst
jne .L2
ret


void t2(U &v) {
// thread using the character
v.character |= 0b1; // set the 1st bit or something
}

t2(U&):
or BYTE PTR [rdi], 1 # NON-ATOMIC RMW of the whole byte.
ret

编写一个运行 v.character |= 1 的程序会很简单。在一个线程中,和一个原子 v.atomic ^= 0b1100000 (或等效于 CAS 循环)在另一个线程中。

如果此代码是安全的,您总是会发现偶数次 XOR 操作只修改高位使它们为零。但你不会发现,因为非原子 or在另一个线程中可能已经执行了奇数次 XOR 操作。或者为了使问题更容易看到,使用加法和 0x10或者别的什么,所以不是 50% 的机会偶然是正确的,你只有 16 分之一的机会是高 4 位是正确的。

当增量操作之一是非原子的时,这与丢失计数几乎完全相同。

Will the cache have to be flushed for the thread that is trying to load the atomic integer?



不,这不是原子性的工作原理。问题不在于缓存,而是除非 CPU 做了一些特殊的事情,否则没有什么可以阻止其他 CPU 在加载旧值和存储更新值之间读取或写入位置。在没有缓存的多核系统上,您会遇到同样的问题。

当然,所有系统都使用缓存,但缓存是一致的,因此有一个硬件协议(protocol) (MESI) 可以阻止不同的内核同时具有冲突的值。当一个存储提交到 L1D 缓存时,它就变得全局可见。见 Can num++ be atomic for 'int num'?详情。

关于c++ - std::atomic 与另一个字符 union ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48678396/

34 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com