结构分配在C / C ++中是原子的吗?

时间:2011-03-31 10:46:52

标签: c++ c struct variable-assignment atomic

我正在编写一个程序,其中一个进程读取和写入共享内存,另一个进程只读取它。在共享内存中有一个这样的结构:


struct A{
    int a;
    int b;
    double c;
};

我期望立即读取结构,因为在我阅读时,其他进程可能正在修改结构的内容。如果结构赋值是原子的,那么可以实现这一点,而不是中断。像这样:


struct A r = shared_struct;

那么,C / C ++中的struct assignment atomic是什么?我尝试在网上搜索但找不到有用的答案。有人可以帮忙吗? 谢谢。

4 个答案:

答案 0 :(得分:17)

不,C和C ++标准都不保证赋值操作是原子的。你需要一些特定于实现的东西 - 编译器或操作系统中的东西。

答案 1 :(得分:9)

C和C ++支持其当前标准中的原子类型。

C ++ 11引入了对atomic types的支持。同样,C11引入了atomics

答案 2 :(得分:2)

您是否需要以原子方式快照所有结构成员?或者您是否只需要单独对单独成员进行共享读/写访问?后者更容易,见下文。

C11 stdatomic和C ++ 11 std::atomic为任意大小的原子对象提供语法。但是,如果它们大于8B或16B,则它们在典型系统上无法锁定。 (即原子载荷,存储,交换或CAS将通过隐藏锁定然后复制整个结构来实现。)

如果你只想要几个成员,最好自己使用一个锁然后访问成员,而不是让编译器复制整个结构。 (目前的编译器并不擅长优化原子的奇怪用法)。

添加一个间接级别,因此有一个指针,可以很容易地原子更新,指向另一个struct,其中包含一组不同的值。 这是RCU (Read-Copy-Update) 的构建基块。另请参阅https://lwn.net/Articles/262464/。 RCU有很好的库实现,所以除非你的用例比一般情况简单得多,否则使用一个而不是自己滚动。弄清楚何时释放结构的旧副本是困难的部分之一,因为在完成最后一个读者线程之前你不能这样做。 RCU的重点是使读取路径尽可能轻......

你的结构在大多数系统上都是16字节;只是勉强够小,以至于x86-64可以比锁定更有效地加载或存储整个东西。 (但只有lock cmpxchg16b)。尽管如此,使用C / C ++原子并不是完全愚蠢的

C ++ 11和C11共同:

struct A{
    int a;
    int b;
    double c;
};

在C11中,使用_Atomic类型限定符来生成原子类型。它是constvolatile等限定符,因此您几乎可以使用它。

#include <stdatomic.h>
_Atomic struct A shared_struct;

  // atomically take a snapshot of the shared state and do something
double read_shared (void) {
    struct A tmp = shared_struct;   // defaults to memory_order_seq_cst
    // or atomic_load_explicit(&shared_struct, &tmp, memory_order_relaxed);

    //int t = shared_struct.a;  // UNDEFINED BEHAVIOUR

    // then do whatever you want with the copy, it's a normal struct
    if (tmp.a > tmp.b)
       tmp.c = -tmp.c;
    return tmp.c;
}

// or take tmp by value or pointer as a function arg
// static inline
void update_shared(int a, int b, double c) {
    struct A tmp = {a, b, c};
    //shared_struct = tmp;
    // If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
    atomic_store_explicit(&shared_struct, tmp, memory_order_relaxed);
}

请注意,访问_Atomic结构的单个成员是未定义的行为。它不会尊重锁定,也可能不是原子的。所以不要int i = shared_state.a;(C ++ 11不要编译,但C11会这样做。)

在C ++ 11中,它几乎相同:使用std::atomic<T>模板。

#include <atomic>
std::atomic<A> shared_struct;

  // atomically take a snapshot of the shared state and do something
double read_shared (void) {
    A tmp = shared_struct;   // defaults to memory_order_seq_cst
    // or A tmp = shared_struct.load(std::memory_order_relaxed);
    // or atomic_load_explicit(&shared_struct, &tmp, memory_order_relaxed);

    //int t = shared_struct.a;  // won't compile: no operator.() overload

    // then do whatever you want with the copy, it's a normal struct
    if (tmp.a > tmp.b)
       tmp.c = -tmp.c;
    return tmp.c;
}

void update_shared(int a, int b, double c) {
    struct A tmp{a, b, c};
    //shared_struct = tmp;
    // If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
    shared_struct.store(tmp, std::memory_order_relaxed);
}

on the Godbolt compiler explorer

如果您不需要对整个结构进行快照,而是只希望每个成员单独原子,那么您可以简单地让每个成员原子类型。 (例如atomic_int_Atomic double or std::atomic<double>)。

struct Amembers {
    atomic_int a, b;
#ifdef __cplusplus
    std::atomic<double> c;
#else
    _Atomic double c;
#endif
} shared_state;
// If these members are used separately, put them in separate cache lines
// instead of in the same struct to avoid false sharing cache-line ping pong.

(注意,C11 stdatomic不保证与C ++ 11兼容 std :: atomic,所以不要期望能够从C或C ++访问相同的结构。)

在C ++ 11中,具有原子成员的结构的struct-assignment不会被编译,因为std::atomic删除了它的拷贝构造函数。 (您应该将std::atomic<T> shared加载到T tmp,就像上面的整个结构示例一样。)

在C11中,具有原子成员的非原子结构的struct-assignment将编译,但不是原子。 C11标准并没有特别指出这一点。我能找到的最好的是:n1570:6.5.16.1简单赋值:

  

在简单赋值(=)中,右操作数的值被转换为类型   赋值表达式并替换存储在左侧指定的对象中的值   操作数。

由于这并未说明原子成员的特殊处理,因此必须假定它与对象表示的memcpy相似。 (除非它允许不更新填充。)

在实践中,很容易让gcc为具有原子成员的结构生成asm,其中它以非原子方式复制。特别是原子成员是原子的,但不是无锁的。

答案 3 :(得分:1)

不,不是。

这实际上是CPU架构相对于被击中的内存布局

的属性

您可以使用“原子指针交换”解决方案,该解决方案可以是原子的,并且可以在无锁方案中使用。 确保将相应的共享指针(变量)标记为易失性,如果其他线程“立即”看到更改很重要 这在现实生活中(TM)不足以保证编译器正确处理。相反,当你想拥有无锁语义时,直接对原子原语/内在函数进行编程。 (请参阅评论和相关文章了解背景信息)

当然,相反,您必须确保在相关时间进行深层复印,以便在阅读方面进行处理。

现在所有这些在内存管理方面很快变得非常复杂,我建议你仔细检查你的设计,然后问自己认真所有(感知到的)性能优势是否能证明风险是合理的。你为什么不选择一个简单的(读/写)锁,或者开始使用线程安全的花哨的共享指针实现?