C语言 访问整型变量时的安全性:1位作者,N位读者

1u4esq0p  于 2023-08-03  发布在  其他
关注(0)|答案(5)|浏览(121)

我有一个“静态64位整型变量”,它只被一个线程更新。
所有其他线程只从它读取。
我应该使用原子操作来保护这个变量吗(例如“__sync_add_and_fetch”)是否出于安全原因?
或者可以阅读(resp)。写)从(或到)它直接?
我仍然很困惑,因为我没有找到一个明确的答案。我不知道该不该保护它:
1.仅在向其写入时
1.对于写入和阅读(__sync_add_and_fetch(V,0))
1.根本不需要保护

czfnxgou

czfnxgou1#

GCC的__sync内建被它的__atomic builtins淘汰了。
读取器和写入器都应该使用__atomic操作,如__atomic_load_n__atomic_store_n(您不需要或想要昂贵的原子RMW,因为只有一个写入器。)使用__ATOMIC_RELAXED,加载和存储与普通操作(无法在寄存器中进行优化)一样便宜。从自旋等待循环中提升负载。
但是在新代码中,通常应该使用C11 stdatomic.h_Atomic类型(https://en.cppreference.com/w/c/thread),以及可用于测试某个原子类型在编译目标上是否无锁的宏。
(With旧的__sync内建,我认为设计意图是您将volatile用于纯加载和纯存储,因为它们没有提供__sync内建。使用volatile手动运行您自己的原子可以在使用GCC和clang等普通编译器的普通ISA上工作,Linux内核依赖于它,但当您可以使用C11 stdatomic.h完成工作时,不建议这样做。

无锁64位原子

#if ATOMIC_LLONG_LOCK_FREE > 0为true的系统上,使用宽松的(或acq/rel)原子,这取决于您需要什么样的排序保证(例如,如果写入器使用计数器向读取器“发布”其他数据,例如:如果计数器用作非原子数组的索引)。

#include <stdatomic.h>
#include <stdint.h>

#if ATOMIC_LLONG_LOCK_FREE > 0

static atomic_uint_fast64_t counter;

// make sure this can inline into readers
uint64_t read_counter() {
   return atomic_load_explicit(&counter, memory_order_relaxed);  // or m_o_acquire
}
void increment_counter_single_writer() { // one thread only
   uint64_t tmp = atomic_load_explicit(&counter, memory_order_relaxed);
   // or keep a local copy of the counter in a register and *just* store.
   // other threads just see the values we store, doesn't matter how we get them
   atomic_store_explicit(&counter, tmp+1, memory_order_relaxed);  // or m_o_release
}
// with multiple writers, use  atomic_fetch_add_explicit

#else
static uint64_t counter;
static _Atomic unsigned seq;
uint64_t read_counter() { ... }
#endif

字符串
请注意,一些32位系统可以执行无锁的64位原子,例如x86(从P5 Pentium开始)和一些ARM 32。看看这是如何在Godbolt上用clang编译x86和ARM Cortex-A8的(随机选择一个不是最近的ARM)。

否则可能是SeqLock

否则,如果没有无锁的64位原子,如果计数器不经常增加,请使用SeqLock。(参见Implementing 64 bit atomic counter with 32 bit atomics)。这仍然使读取器是真正的只读,因此它们不会相互争用缓存行,它们只需要在写入器处于更新过程中时尝试读取时重试。(在写入器完成之后,包含计数器该高速缓存行可以在运行读取器线程的所有核上处于共享状态,因此它们都可以在高速缓存中获得命中。

对于单调计数器,计数器本身的一半可以用作序列号以检测撕裂。就像在阅读高半部分之前和之后读取低半部分一样,如果不同,则重试。
读取器/写入器锁将迫使读取器彼此竞争以修改持有锁该高速缓存行,因此总吞吐量不会随着读取器的数量而缩放。如果你有一个非常频繁修改的计数器(所以seqlock经常处于不一致的状态),你可能会考虑更聪明的东西,比如一个最近值的队列,这样读者就可以检查最近的一致值或其他东西。
顺便说一句,ATOMIC_LLONG_LOCK_FREE > 0似乎是合适的:ATOMIC_LLONG_LOCK_FREE == 1的意思是“有时无锁”,但实际上并没有一些对象是无锁的,而另一些不是。如果有的话,我们希望编译器可以安排一个松散的全局/静态变量进行对齐,使其成为原子变量。

x4shl7ld

x4shl7ld2#

通常,阅读和写入存储位置的原子性是相同的。
也就是说,如果一个位置不能被原子地写入,它也不能被原子地读取,反之亦然。
如果需要特殊的原子写入,则使用它毫无意义,除非读取也是原子的。
例如,设想使用需要两次32位访问的普通读取来读取64位位置。假设写操作发生在这两次访问之间。读取操作将从新值中取出第二个32位,并将其与过时的第一个32位组合。写入不能介于读取的两半之间的唯一方式是读取是原子的。原子读取知道如何正确地与原子写入交互以防止这种情况。
您可能会被这条规则的“例外”所迷惑。在某些系统中,您可能会看到原子更新(如增量)与普通读取混合在一起。这是基于这样的假设,即读和写实际上是原子的;特殊的原子增量仅用于使读/修改/写周期从并发写入器的Angular 看是不可分割的:如果N个写入器大约同时执行该递增,则它确保该位置递增N。
有时候,您可能还会看到正确的优化,即使底层数据类型不是原子访问的,也会使用普通的读取。在这种情况下,算法并不关心它是否读取了一个“半成品”值。
例如,为了简单地监视内存位置以检测更改,您不需要原子读取。检测更改不需要检索正确的值。例如,0x 00000000的示例被更新为0x 00010001,但是非原子读取观察到中间值0x 00010000,这对于检测位置已经改变来说仍然足够好。
如果您必须确保读取器永远不会看到不完整的值,请使用原子读取和写入。
还有其他的问题,比如订购。假设写入器更新了两个位置A和B。在一些计算系统中,读者可能在A之前观察到B的更新。除了任何原子更新指令之外,还必须使用特殊的“屏障”或“栅栏”指令。
在更高级的语言中,这些障碍的API可能内置在一些原子操作的语义中,因此,您可能最终只是为了这些障碍而使用原子指令,即使数据在其他方面是原子的。

yx2lnoni

yx2lnoni3#

作为一个简单的答案,我会建议你保护它(不,它不可以直接写),只有当你写它。
虽然你知道如果你正在做这样的事情,线程不同步,你可能会得到不同的结果每次。
PS.我想这是一个评论,但我的代表太低

qjp7pelc

qjp7pelc4#

是的。你需要一个读写器锁。他们就是这么做的块,而写作和其他读者可以阅读他们的乐趣
如果你使用的是boost,我相信应该是boost::shared_mutex
C++目前不支持读写器锁,尽管你可以自己实现它们。
有关读写器锁的更多信息,请参阅here

cotxawn7

cotxawn75#

对于64位目标体系结构,它具有用于整体读取/写入64位值的本机指令,您不需要像互斥锁或seq锁这样的保护。但为了强制编译器使用64位访问指令,避免读写变量时延迟内存访问,应采取一些预防措施。如果代码使用C11标准或更高版本编译,原子(具有宽松的内存顺序)将是最佳选择:它们提供上述所有保障。对于较旧的编译器,变量的volatile说明符在大多数情况下可以提供这些保证。
否则,对于32位体系结构,您应该在更新器线程中的写入和读取器线程中的读取周围使用某种临界区。它可以是互斥锁、读写锁、序列锁等。
(也有支持64位访问的特殊指令的32位体系结构。对于这些目标,解决方案可以是特定于体系结构的。)

相关问题