Reputation: 2968
So I wanted to try implementing a fixed sized, wait free stack with the fetch_add
and fetch_sub
atomic instructions. Say that I have a basic stack, with two operations, push and pop.
struct WaitFreeStack {
std::atomic<size_t> cur;
std::atomic<int>* buf;
void push(int i) {
buf[cur.fetch_add(1)].store(i);
}
int pop() {
return buf[cur.fetch_sub(1) - 1].load();
}
};
My question is, are operations in the form of B[X]
, where B is an array and X is an integer atomic ? In my example for instance, is it possible that after a fetch_add
call for the push()
method is executed, and before the B[X]
is executed, that a whole pop
and push
in separate threads could be executed, causing a push
to overwrite another push
?
Upvotes: 0
Views: 435
Reputation: 4243
B[X]
is not atomic. But if it even would be atomic, it wouldn't help much.
The problem is that you have expression of multiple atomic operations. Although operations are atomic, the whole expression is not atomic.
Or: an expression containing of multiple atomic operations does not need to be atomic.
The class invariant here should be that cur
points to the current object in buf
.
But this invariant is broken between 2 atomic operations fetch_add
and store
.
If B[X]
would be atomic (which is not), one would have following sequence of atomic operations for push:
X = cur.fetch_add(1); // atomic
// 1. dT
ref = B[X]; // let's assume atomic
// 2. dT
ref.store(i); // atomic
For example in the time interval 2.dT imagine second thread popping 2 items and third thread pushing 1 item, all executing before ref.store(i)
. In this time the value under ref
reference would change.
Upvotes: 0
Reputation: 3763
are operations in the form of B[X], where B is an array and X is an integer atomic ?
No.
In my example for instance, is it possible that after a fetch_add call for the push() method is executed, and before the B[X] is executed, that a whole pop and push in separate threads could be executed, causing a push to overwrite another push ?
Yes.
Your example can be equated to:
void push(int i) {
size_t index = cur.fetch_add(1);
// execution time interval
buf[index].store(i);
}
int pop() {
size_t index = cur.fetch_sub(1) - 1;
// execution time interval
return buf[index].load();
}
There will be an execution time interval at both comment positions above, although the time interval is very very short, but if another thread calls push
or pop
and completes the call at this time, it will be absolutely unsafe.
The easiest way to implement thread-safe containers is with std::mutex
, and there are some lock-free implementations (like boost).
Upvotes: 1