原子结构体和指针的误解

14

我的第一个问题是:是否有任何方法可以访问atomic<struct>对象中的结构体成员? 例如,我会收到编译器错误:

struct std::atomic<node>’ has no member named ‘data’ a.data = 0; 

在这一部分中

struct node{
  int data;
  node* next;
};

int main(){
  atomic<node> a;
  a.data = 0;
}

我可以通过创建一个临时节点来解决这个问题,方法如下:

  atomic<node> a;
  node temp;
  temp.data = 0;
  a.store(temp);

但这似乎不太优雅。

第二个问题是,如果我有一个指向原子对象的指针,有没有办法直接访问节点的成员?显然以下代码无法编译,我该如何更改以将0存储在b处节点的值中?

atomic<node> b = new node;
b->data = 0;

这是我找到的一个解决方案,但还有更优雅的方法吗?

atomic<node> *b;
node temp;
temp.data = 0;
b->store(&temp);

最后,atomic<node*>atomic<node>*之间的区别是什么?


不,只有一组有限的原子操作(加载、存储、交换等)。 - user2249683
2
atomic<node*> 强制原子更新它所持有的指针(而不是指针所指向的内容)。atomic<node>* 是一个指向 atomic<node> 的指针,其目的是强制原子更新 node 对象。 - Barry
2
如果你想要一个结构体,它可以原子性地更新两个成员,或者单独地原子性修改其中一个(而不需要在整个结构体上进行compare_exchange_weak操作),你可以使用一个原子结构与一个具有两个原子成员的结构体的union。(如果你使用的是保证写入一个联合成员然后读取另一个成员是可行的C++编译器,就像在C99中一样)。这实际上可以高效地处理结构体的最大大小,即在x86-64上为16B的硬件cmpxchg。 - Peter Cordes
3个回答

13

这种[解决方法]似乎不太优雅。

std::atomic<T>无法使任意操作变为原子操作:只支持加载和存储数据。这就是为什么你的“解决方法”实际上是处理原子对象的方式:你可以以任何你喜欢的方式准备新的node值,然后将其原子地设置到atomic<node>变量中。

如果我有一个指向原子对象的指针,有没有办法直接访问节点的成员?

通过指针访问节点内容也不是原子的,因为std::atomic<T>仅保证将其值加载和存储为原子操作,它不允许您访问T的成员而不进行显式复制。这是一件好事,因为它防止代码读者错误地认为对T的内部访问是原子的。

atomic<node*>atomic<node>*之间有什么区别?

在第一种情况下,原子对象存储一个指针,可以原子地访问它(即可以将此指针重新指向新节点)。在第二种情况下,原子对象存储可以原子地访问的值,这意味着您可以原子地读取和写入整个node


1
那么,如果我想要原子地对成员进行更改,我应该将成员声明为原子的,而不是结构体? - Matt Pennington
1
@MattPennington 没错!如果你想要访问node内部的数据是原子性的,那么你的node应该长这样:struct node {atomic<int> data;}; - Sergey Kalinichenko
只要你可以接受在任何情况下都会给使用你的结构体的人带来原子负担,那么这样做就没问题。如果你只关心在一些特定的上下文中线程安全,那么最好还是使用单独的互斥锁。 - dlf
现在这就有意义多了。谢谢。 - Matt Pennington

2

当你进行操作时

atomic<node> a;
node temp; // use a.load() to copy all the fields of a to temp
temp.data = 0;
a.store(temp);

您失去了“next”字段的值。我建议您进行以下更改。如果节点是一个简单类型,比如std::atomic_int,我认为使用“=”运算符是可能的。否则不行。我认为在您的情况下没有其他解决方法。
最后,atomic和atomic*之间有什么区别?
如果您使用atomic,则对节点对象地址执行的操作将是原子操作,而在另一种情况下,您需要为原子对象分配内存,并且对实际节点对象执行的操作将是原子操作。

1
请注意,您的“解决方案”包括非原子读取修改写入除.data之外的所有成员。
atomic<node> a;

node temp = a.load();
temp.data = 0;
a.store(temp); // steps on any changes to other member that happened after our load

如果您想要一个结构体,其中您可以原子地同时更新所有成员,或者分别修改其中一个(而不需要在整个结构上使用compare_exchange_weak),那么您可以使用 原子结构和具有两个原子成员的结构的联合。这对于双向链表中的指针或指针+计数器等情况可能很有用。当前的编译器甚至在只读取原子结构的一个成员时也很慢,例如在gcc6.2上即使使用memory_order_relaxed。(这是gcc6.2的情况,即使在memory_order_relaxed下,编译器也会使用CMPXCHG16B加载整个结构然后仅查看一个成员。)

这种联合技巧仅在您使用保证写入一个联合成员,然后读取另一个成员是可以的C ++编译器中有效,就像在C99中一样。

对于最大尺寸不超过硬件cmpxchg限制的结构体(即在x86-64上为16B,如果您在gcc中启用了-mcx16,则使用CMPXCHG16B,第一代K8 CPU不支持,因此不是基线的x86-64),此方法适用。

对于较大的结构体,atomic<the_whole_thing>将无法保证无锁,并且通过另一个联合成员中的atomic<int>读取/写入其成员将不安全。尽管读取可能还可以,但这可能会使内存排序语义混乱,因为即使是有强顺序的x86也可以重新排列完全包含它的窄存储器与宽加载器。如果您只需要原子性,那么这很好,但在同一线程中读取整个对象(例如在执行cmpxchg时)需要在x86上进行MFENCE才能实现获取/释放语义。您始终会看到自己的存储,但如果其他线程正在向同一对象存储,则它们可以将您的存储观察为发生在您的加载之后。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接