Misunderstanding of atomic structs and pointers

2019-04-18 18:37发布

问题:

My first question is: Is there any way to access the members of struct in an atomic<struct> object? For example, I get the compiler error:

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

in this segment

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

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

I can work around it by creating a temporary node like so:

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

but this doesn't seem very elegant.

The second question is, what if I have a pointer to an atomic object? Is there anyway to access the members of the node directly? Obviously the following does not compile, how would I change this to store 0 in the value of the node at b?

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

This is a solution I've found, but again, is there a more elegant way of doing this??

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

And lastly, what is the difference between atomic<node*> and atomic<node>*

回答1:

this [workaround] doesn't seem very elegant.

std::atomic<T> cannot make arbitrary operations atomic: only loading and storing the data is supported. That is why your "workaround" is actually the way to deal with atomic objects: you prepare the new node value in any way that you like, and then atomically set it into an atomic<node> variable.

what if I have a pointer to an atomic object? Is there anyway to access the members of the node directly?

Accessing the content of a node through a pointer would not be atomic as well: since std::atomic<T> can guarantee only loading and storing its value to be atomic, it does not let you access T's members without making an explicit copy. This is a good thing, because it prevents readers of the code from getting a false impression that the access to T's internals is somehow atomic.

what is the difference between atomic<node*> and atomic<node>*

In the firs case, the atomic object stores a pointer, which can be accessed atomically (i.e. you can re-point this pointer to a new node atomically). In the second case, the atomic object stores the value that can be accessed atomically, meaning that you can read and write the entire node atomically.



回答2:

When you do

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

you loose the value of next field. I'd make the suggested change. If node would have been a simple type, like std::atomic_int, I think that using the "=" operator would have been possible. Otherwise not. I don't think there's another workaround for your case.

And lastly, what is the difference between atomic < node* > and atomic < node > *?

If you use atomic < node* > the operations done on the address of a node object will be atomic while in the other case you need to allocate memory for the atomic object and the operations done on the actual node object will be atomic.



回答3:

Note that your "solution" includes a non-atomic read-modify-write of all the members other than .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

If you want a struct where you can atomically update all members together or separately atomically modify one of them (without a compare_exchange_weak on the whole struct), you can use a union of an atomic struct and a struct with two atomic members. This might be useful for e.g. both pointers in a double-linked list, or a pointer + counter. Current compilers are bad at even reading one member of an atomic struct without doing something slow like using CMPXCHG16B to load the whole struct and then just look at one member. (That's the case on gcc6.2 even with memory_order_relaxed).

This union hack only works if you're using a C++ compiler that guarantees that writing one union member and then reading another is ok, like it is in C99.

This works for structs up to the max size the hardware can cmpxchg, i.e. 16B on x86-64 (if you enable -mcx16 in gcc, to use CMPXCHG16B which the first generation of K8 CPUs didn't support, so it's not technically baseline x86-64).

For larger structs, atomic<the_whole_thing> won't be lock-free, and reading/writing to members of it through atomic<int> in another union member will not be safe. Reading may still be ok, though.

This may make a mess of the memory-ordering semantics, because even strongly-ordered x86 can reorder a narrow store with a wider load that fully contains it. If you mostly just need atomicity, it's great, but reading the full object (e.g. while doing a cmpxchg) in the same thread that just wrote one member requires an MFENCE on x86 even for acquire/release semantics. You will always see your own store, but if other threads are storing to the same object, they can observe your store as happening after your load.