C/C++ 中的结构赋值是原子的吗?
Posted
技术标签:
【中文标题】C/C++ 中的结构赋值是原子的吗?【英文标题】:Is struct assignment atomic in C/C++? 【发布时间】:2011-07-26 19:09:25 【问题描述】:我正在编写一个程序,它有一个进程读取和写入共享内存,而另一个进程只读取它。在共享内存中有一个这样的结构:
struct A
int a;
int b;
double c;
;
我希望立即阅读该结构,因为在我阅读时,其他进程可能正在修改该结构的内容。如果结构分配是原子的,即没有中断,则可以实现这一点。像这样:
struct A r = shared_struct;
那么,我尝试在网上搜索,但找不到有用的答案。任何人都可以帮忙吗? 谢谢。
【问题讨论】:
即使分配单个字符在 C 或 C++ 中也不是原子的。 @Serge:更准确地说,C99 根本没有定义,这取决于 CPU 内存模型。 C1x,OTOH,明确定义如果类型不是原子类型,则不能保证它是原子的。 【参考方案1】:不,C 和 C++ 标准都不保证赋值操作是原子的。为此,您需要一些特定于实现的东西——无论是在编译器中还是在操作系统中。
【讨论】:
或者你需要 C++11std::atomic<A> shared_struct;
或 C11 _Atomic struct A shared_struct;
看我的回答。【参考方案2】:
C 和 C++ 在其当前标准中支持原子类型。
C++11 引入了对atomic types 的支持。同样 C11 引入了atomics。
【讨论】:
我想知道为什么这是 d/v'ed 据我了解sig_atomic_t
(C89) 保证原子访问,对吧? (并不是说这对 OP 有帮助)
这个答案自相矛盾,只是总体上令人困惑
@rubenvb 在我的辩护中,我 5 年前写的时候是准确的。期望我们在时间的蹂躏下保持每一个答案是不现实的。这个网站是一个维基。你为什么不简单地编辑答案?那将是社区精神要做的事情。为什么不同时解决另一个答案。【参考方案3】:
您是否需要对所有结构成员进行原子快照?还是您只需要分别对不同成员的共享读/写访问权限?后者要容易得多,见下文。
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
类型限定符来创建原子类型。它是一个限定符,例如 const
或 volatile
,因此您几乎可以在任何东西上使用它。
#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 tmpa, 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 中,具有原子成员的结构的结构赋值不会编译,因为 std::atomic
删除了它的复制构造函数。 (您应该将 std::atomic<T> shared
加载到 T tmp
中,就像上面的整体结构示例一样。)
在 C11 中,具有原子成员的非原子结构的结构赋值将编译,但 不是原子的。 C11 标准并没有在任何地方特别指出这一点。我能找到的最好的是:n1570:6.5.16.1 简单分配:
在简单赋值 (=) 中,右操作数的值被转换为 赋值表达式并替换左边指定的对象中存储的值 操作数。
由于这并没有说明原子成员的特殊处理,因此必须假定它就像对象表示的memcpy
。 (除非允许不更新填充。)
在实践中,很容易让 gcc 为具有原子成员的结构生成 asm,并在其中以非原子方式复制。尤其是具有原子但不是无锁的大型原子成员。
【讨论】:
【参考方案4】:不,不是。
这实际上是 CPU 架构相对于被击中的内存布局的一个属性
您可以使用“原子指针交换”解决方案,它可以变成原子的,并且可以在无锁场景中使用。
如果其他线程“立即”看到更改很重要,请务必将相应的共享指针(变量)标记为 volatile这在现实生活中 (TM) 不足以保证由编译器正确处理。相反,当您想要无锁语义时,直接针对原子原语/内在函数进行编程。 (背景见 cmets 和链接文章)
当然,相反,您必须确保在相关时间进行深层复制,以便在阅读方面进行处理。
现在,与内存管理相关的所有这些都很快变得非常复杂,我建议您仔细检查您的设计并认真问问自己,所有(感知到的?)性能优势是否足以证明风险是合理的。为什么不选择一个简单的(读取器/写入器)锁,或者动手实现一个线程安全的共享指针?
【讨论】:
volatile 在现代编译器上是不够的。您还需要某种 membar 或栅栏指令(内联汇编程序)。 (可以说,volatile 应该就足够了,而且我认为 VC++ 的最新版本比我使用的版本足够,但对于 VC++ 2005、g++ 或 Sun CC 来说还不够。) -1 表示创建指针volatile
的危险建议。我们之前已经多次讨论过volatile
。做一点阅读。众多示例之一:***.com/questions/4136900/…
@John/@James:好的——这很有教育意义。最有趣的部分是 C++11 即将发生的变化。我会留下我的答案警告,以便任何新读者都能获得信息。 @VJo:也很好……
@sehe:你的编辑很棒;我会将我的 d/v 反转为 u/v,因为这是一个非常有用的响应。以上是关于C/C++ 中的结构赋值是原子的吗?的主要内容,如果未能解决你的问题,请参考以下文章
POSIX 的 read() 和 write() 系统调用是原子的吗?