A common problem with concurrency is the so-called ABA problem. This means reading a variable twice, returning the same value A each time. Therefore, one comes 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 enjoys writing articles on the programming languages C++, Python, and Haskell, but also frequently speaks at expert conferences. On his blog Modern C++ he discusses his passion C++ in depth.
The following scenario presents the problem.
an analogy
The scenario is that you are sitting in your car and waiting for the traffic light to turn green. In our case, green means B and red means A. What will happen next?
- You look at a traffic light and it is red (A).
- Because it’s boring, you watch the news on your smartphone and forget about the time.
- You look at the traffic lights again. Damn it, he’s still red (A).
The traffic light turned green between the two checks (B). So there were two red phases, although there appeared to be only one.
What does it look like with threads (processes)? Now again very formally.
- Thread 1 reads a variable
var
With value A. - Thread 1 is interrupted and thread 2 is running.
- Thread 2 changes variable
var
From A to B to A. - Thread 1 starts execution and checks the value of the variable
var
Since the value of the variablevar
remains the same, thread 1 continues its work,
Often you can ignore ABA.
nuclear multiplication
In the code below, the function multiply is 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 behaviour:
- if comparison
false
will resultexpected
Butshared
set. - If nuclear comparison
true
will resultshared
in a single nuclear operationexpected
set.
The most important observation is that there is a small time period between reading the old value T oldValue = shared.load
(2) and comparison with the new value (3). So another thread can step in and oldValue
From oldValue
To anotherValue
and back again oldValue
Change. anotherValue
Have B in ABA.
I would like to describe ABA using a lock-free data structure.
a lock-free stack
I am using a lock-free stack implemented as a linked list. The stack supports only two operations.
- Pop the top object and return a pointer to it.
- 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 goes through the following steps until successful.
- Get head node: Head
- Get the following nodes: headnext
- Make headnext If the new chief Head still the head of the pile
Here are the first two nodes 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 becomes active.
Stack: TOP -> B -> C
- Thread 2 deletes B and deletes B
Stack: TOP -> C
- Thread 2 pushes A back
Stack: TOP -> A -> C
Thread 1 has been rescheduled to check A == head
Since A == head
becomes headNext
That means B, for the new chief. But B has already been removed. Therefore the behavior of the program is undefined.
There are some solutions to the ABA problem.
Solution for ABA
The conceptual problem of ABA is relatively easy to understand. lumpy B == headNext
Even if another node is deleted, A == head
told. The solution to our problem is to prevent premature deletion of a node. Here are some possible solutions.
- Reference to marked position
One can add a tag indicating how many times the node was successfully changed. However, the compare and swap method ultimately fails, even though the check returns (/code) true (/code).
The next three techniques are based on the idea of delayed recapture.
Garbage collection guarantees that variables are only deleted when they are no longer needed. This sounds promising, but it has one major 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 threat indicator system, each thread maintains a list of threat indicators that indicate which nodes the thread reaches. (This “list” may be limited to only one or two items in many systems.) Nodes on the threat indicator list cannot be modified or released by another thread. …when a thread wants to delete a node, it puts it on the list of nodes to be “released later”, but does not release the node’s memory until another thread’s delete list May not contain a pointer. A dedicated garbage collection thread can perform this manual garbage collection (if the “release later” list is shared among all threads); Alternatively, cleanup of the release list may be performed by each worker thread as part of an operation such as pop.
rcu means Reed Cmake a copy of Youpdate, a synchronization technique for almost read-only data structures developed by Paul McKechnie and used in the Linux kernel since 2002.
The idea is quite simple and follows the acronym. To change data, you make a copy of the data and change that copy. In contrast, all readers work with the original data. If there is no reader, the data structure can be safely changed from the copy.
What will happen next?
In my next article I will implement a lock-free stack with deferred reclamation.
(rme)