A common problem with consistency is the so-called ABA problem. This means that you read a variable A twice always returning the same value A. Therefore, you come to the conclusion that nothing has changed in between. But you forgot b.
Advertisement
Rainer Grimm has been working as a software architect, team and training manager for many years. He likes to write articles on the programming languages ​​C++, Python and Haskell, but also likes to speak at expert conferences. On his blog Modern C++, he deals in depth with his passion C++.
The following scenario presents the problem.
an analogy
The scenario looks like you sit in your car and wait for the traffic light to turn green. In our case, Green stands for B and Red for A. What’s next?

- You look at the traffic lights and it’s red (A).
- Because it’s boring, you look at the news on your smartphone and forget the time.
- You see the traffic light again. Damn it’s still red(a).
Between the two checks the traffic light turns green (B). So there were two red phases, although it seemed just one.
What about threads (processes)? Now again very formally.
- Thread 1 reads a variable
var
With value A. - Thread 1 is interrupted and thread 2 is done.
- thread 2 changes variable
var
A to B. - Thread 1 starts with execution and checks the value of the variable
var
Since the value of the variablevar
The same remains, thread 1 continues its work,
Often you can ignore ABA.
nuclear multiplication
In the following code, the function multiplies fetch_mult
(1) A std::atomic
that mult
is shared.
// fetch_mult.cpp
#include
#include
template
T fetch_mult(std::atomic& shared, T mult){ // 1
T oldValue = shared.load(); // 2
while (!shared.compare_exchange_strong(oldValue, oldValue * mult)); // 3
return oldValue;
}
int main(){
std::atomic myInt{5};
std::cout << myInt << '\n';
fetch_mult(myInt,5);
std::cout << myInt << '\n';
}
shared.compare_exchange_strong(expected, desired)
(3) Have the following behavior:
- if comparison
false
result, willexpected
Butshared
set. - If nuclear comparison
true
result, willshared
in the same nuclear operationexpected
set.
The most important observation is that there is a small time window between reading the old value T oldValue = shared.load
(2) and comparison with the new value (3). Therefore, another thread can step in and oldValue
From oldValue
To anotherValue
and again oldValue
Change. anotherValue
B is in ABA.
I would like to describe ABA based on a lock-free data structure.
a lock-free stack
I use a lock-free stack that is implemented as a chained list. The stack supports only two operations.
- Pops the top object and returns a pointer.
- Push the specified object onto the stack.
I would like to describe the pop operation in pseudocode to give you an idea of ​​the ABA problem. The pop operation performs the following steps in a loop until it succeeds.
- Get the head knot: Head
- Get the following nodes: headnext
- Make headnext When the new head Head still the head of the stack
Here are the first two knots of the stack:
Stack: TOP -> head -> headNext -> ...
aba in action
Let’s start with the following stack:
Stack: TOP -> A -> B -> C
Thread 1 is active and wants to remove the stack head.
Before thread 1 finishes the pop algorithm, thread 2 is active.
Stack: TOP -> B -> C
- Thread 2 removed B and deleted B
Stack: TOP -> C
- Thread 2 pushes one back
Stack: TOP -> A -> C
Thread 1 is new to check if A == head
da a == head
becomes headNext
So B, for the new head. But B has already been removed. Therefore, the program has an undefined behavior.
There is some help for the ABA problem.
Solution for ABA
The conceptual problem of ABA is relatively easy to understand. like a knot B == headNext
Although another knot was removed, A == head
Referred to it. The solution to our problem is to prevent the knot from being removed prematurely. Here are some solutions.
- reference to a marked state
You can add a day to indicate how many times the knot has been successfully changed. However, the “compare and exchange” method fails at some point, although the review (/code) is correct (/code).
The next three techniques are based on the idea of ​​deferred recapture.
Garbage collection guarantees that variables will only be deleted when they are no longer needed. This sounds promising, but there is one big disadvantage. Most garbage collectors are not lock-free. So, you have a lock-free data structure, but the overall system is not lock-free.
From Wikipedia: Danger signs:
In a dangerous-pointer system, each thread leads a list of dangerous pointers that indicate which nodes the thread accesses. (This “list” may be limited to only one or two elements in many systems.) Lumps on the dangerous pointer list must not be changed or released by another thread. …If a thread wants to remove a knot, it relies on a list of knots that “must be released later”, but only releases the node’s memory if there are no other threads in the threat list. Is. A dedicated garbage collection thread can perform this manual garbage collection (if the list “release later” is shared by all threads); Alternatively, clearing the “shared” list can be done by each task thread as part of an operation such as “pop”.
RCU stands for READ Cmake a copy of YouPDate, a synchronization technique developed by Paul McKechnie and used in the Linux kernel since 2002 for nearly write-protected data structures.
The idea is quite simple and follows the acronym. To change data, make a copy of the data and change this copy. In contrast, all readers work with the original data. If there is no reader, the data structure can be replaced by copying without hesitation.
What will happen next?
In my next article I will implement a lock-free stack with deferred reclamation.
(rme)
