5

I am writing a copy constructor for a data structure which needs to copy two std::atomic<T> members into a new object. While the process doesn't necessarily have to be atomic in my use-case, I would prefer to have the most correct solution possible.

I am aware that the copy constructor is explicitly deleted with std::atomic<T> so as to force users to use the atomic interface.

atomic(const atomic&) = delete;

What I am currently I am doing something like this:

SomeObject(const SomeObject& other): 
   _atomic1(other._atomic1.load()),            
   _atomic2(other._atomic2.load()) {
...
}

I do not believe this operation is atomic, nor do I know a way to make is so (without locks).

Is there a way to copy these values atomically (without locks)?

Don Scott
  • 3,179
  • 1
  • 26
  • 40
  • There's no general way to atomically copy two random atomic objects, if that's what you are asking. Add a mutex if you actually need an atomic copy. – Matteo Italia Nov 15 '15 at 18:17
  • 3
    9 times out of 10, the answer to this question will be "you don't need to do that. you're going about whatever you are doing the wrong way". If the data structure is some kind of collection, for example, then it will already have a way to get at its contents in a concurrency-safe way, and you should use that. – Matt Timmermans Nov 15 '15 at 18:38
  • @MattTimmermans This is a great point, albeit my question is academic in nature. **Viewers of this question should take note and use types designed by experts if possible.** Concurrency is hard, so if you write something yourself get it peer reviewed. – Don Scott Nov 15 '15 at 19:02
  • 1
    The point of atomic variables is to be used as *shared state* (typically, but not exclusively, for purposes of synchronization). It's not obvious what it should mean to copy such a state. TLDR: What you want is wrong. – Kerrek SB Nov 15 '15 at 20:32
  • @KerrekSB As mentioned, the question was merely academic. My use case doesn't require copying. – Don Scott Nov 15 '15 at 22:10
  • @DonScott: Yes, I get that, but I wanted to be clear that your request for "the most correct solution" doesn't really make sense because there isn't a problem here that's calling for a solution. – Kerrek SB Nov 15 '15 at 22:15
  • @KerrekSB I am not sure you understand my question then. The problem of taking an object which has a shared state and duplicating it is not totally ridiculous. You could, for example, have a simple shared data-structure implemented with atomics. The question was therefore: _is there a way to copy this?_ The answer was _no._ That does not mean that _"what I want is wrong"_ it just means that it cannot be done with this approach. – Don Scott Nov 15 '15 at 23:18
  • What do you mean by the _most correct_ solution? Is it kind of like being _mostly pregnant_? Your requirements are either that the two values must be copied atomically, or they aren't. I could imagine some scenarios where as a QoI issue copying them atomically most of the time helps with some aspect, but in that case know that even plain consecutive reads will get that behavior most of the time. – BeeOnRope Sep 05 '17 at 18:49

1 Answers1

4

The only way is to make a trivially copyable struct S containing two Ts and use std::atomic<S>.

Note that this only works if you've been using this S from the start - there is no way to atomically load two separate atomics without locks.

So instead of:

struct SomeObject {
    SomeObject(const SomeObject& other) : i(other.i.load()), j(other.j.load()) { }
    std::atomic<int> i, j;
};

Do this:

struct SomeObject {
    SomeObject(const SomeObject& other) : data(other.data.load()) { }
    struct Data { int i, j; };
    std::atomic<Data> data;
};

Note that this might (probably will) still use locks internally. Use is_lock_free to check if it does.

orlp
  • 112,504
  • 36
  • 218
  • 315
  • Hmm. That's creative, but I do suspect it uses locks internally. I'll try it out. – Don Scott Nov 15 '15 at 18:29
  • @DonScott [For <= 64 bit structs it seems to be lockfree, but not larger.](http://coliru.stacked-crooked.com/a/e496ba55f9a710a8) – orlp Nov 15 '15 at 18:34
  • I have the same result, but viewers should note that **this behavior is compiler and architecture dependent.** Aligned 8 Bytes read/writes are generally atomic on recent hardware (even on 32 bit operating systems), but users should take care to verify their systems. – Don Scott Nov 15 '15 at 19:07
  • @DonScott: avoiding locks at all cost makes sense if you did your tests and verified that you have performance problems due to lock contentions. If that's not the case, having a lock is not going to be a problem. – Matteo Italia Nov 15 '15 at 19:53
  • 1
    Note that this makes loading `i` or `j` separately slower, especially in 32-bit code when compilers still insist on loading `data` with a single 64-bit atomic load. See https://stackoverflow.com/questions/38984153/how-can-i-implement-aba-counter-with-c11-cas/38991835#38991835 for a union hack to allow efficient access to a single member, but also allow CAS of both. It should work on compilers where union type-punning is safe. For x86-64, it requires a compiler that uses `cmpxchg16b` for a 16-byte CAS, and where 16-byte structs are lock-free. – Peter Cordes Sep 05 '17 at 00:37