Skip to content

Commit b9ddce2

Browse files
committed
Add a simple ABA problem example and more description
Because ABA problem is not so intuitive, the plan is to have a simple example first, then reusing the new example in section 5. A nother stack example by "null program" will be mentioned for additional examples. Next commit will provide solutions to fix ABA problem in both examples.
1 parent 73216aa commit b9ddce2

File tree

2 files changed

+91
-10
lines changed

2 files changed

+91
-10
lines changed

concurrency-primer.tex

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -886,25 +886,67 @@ \subsection{Conclusion about lock-free}
886886
\subsection{ABA problem}
887887
We have introduced CAS as one of the read-modify-write operations.
888888
However, does the target object not changing really mean that no other threads modified it halfway through?
889+
If the target object is changed to something by other thread and changed back, the result of comparison is still equal.
890+
Although the target object has indeed been changed, causing the operation not remaining atomic.
891+
We call this \introduce{ABA problem}.
889892
Consider the following scenario,
890893

894+
\inputminted{c}{./examples/simple_aba_example.c}
895+
896+
Compile it with \monobox{gcc -std=c11 -Wall -Wextra -pthread simple\_aba\_example.c}.
897+
The execution result would be:
898+
899+
\begin{ccode}
900+
A: v = 42
901+
B: v = 47
902+
B: v = 42
903+
A: v = 52
904+
\end{ccode}
905+
906+
In the example provided, the presence of ABA problem results in thread A being unaware that variable \monobox{v} has been altered.
907+
Since the comparison result indicates \monobox{v} unchanged, \monobox{v + 10} is swapped in.
908+
Here sleeping is only used to ensure the occurance of ABA problem.
909+
In real world scenario, instead of sleeping, thread A could paused by being context switched for other tasks, including being preempted by higher priority tasks.
910+
This example seems harmless, but things can get nasty when atomic \textsc{RMW} operations are used in more complex data structures.
911+
912+
In a broader context, the ABA problem occurs when changes occur between loading and comparing, but the comparing mechanism is unable to identify that the state of the target object is not the latest, yielding a false positive result.
913+
914+
Back to thread pool example in \secref{rmw}, it contains ABA problem as well.
915+
In \monobox{worker} function, we have the thread trying to claim the job.
916+
891917
\begin{ccode}
892-
/* example code place holder */
893-
do { /* read and modify */ }
894-
/* something can happen in between */
895-
while(CAS);
918+
job_t *job = atomic_load(&thrd_pool->head->prev);
919+
while (!atomic_compare_exchange_strong(&thrd_pool->head->prev, &job,
920+
job->prev)) {
921+
}
896922
\end{ccode}
897923

898-
If the target object is changed to something by other thread and changed back, the result of comparism is still equal.
899-
Although the target object has indeed been changed, causing the operation to not remain atomic.
900-
We call this \introduce{ABA problem}. In the example above, the presents of ABA problem leads to
924+
Consider the following scenario:
925+
\begin{enumerate}
926+
\item There is only one job left.
927+
\item Thread A loads the pointer to the job by \monobox{atomic\_load()}.
928+
\item Thread A is preempted.
929+
\item Thread B claims the job and successfully updates \monobox{thrd\_pool->head->prev}.
930+
\item Thread B sets thread pool state to idle.
931+
\item Main thread fininshes waiting and adds more jobs.
932+
\item Memory allocator reuses the recently freed memory as new jobs addresses.
933+
\item Fortunately, the first added job has the same address as the one Thread A holded.
934+
\item Thread A is back in running state. The comparison result is equal so it updates \monobox{thrd\_pool->head->prev} with the old \monobox{job->prev}, which is already a dangling pointer.
935+
\item Another thread loads the dangling pointer from \monobox{thrd\_pool->head->prev}.
936+
\end{enumerate}
901937

902-
The ABA problem occurs when changes occur between reading and comparing, but the comparing mechanism is unable to identify that the state is not the latest.
903-
The maximum number we refer to may not be the actual maximum, and the next job may not be the same job.
904-
Failure to recognize this through comparison can result in outdated information.
938+
Notice that even though \monobox{job->prev} is not loaded explicitly before comparison, compiler could place loading instructions before comparison.
939+
At the end, the dangling pointer could either point to garbage or trigger segmentation fault.
940+
It could be even worse if nested ABA problem occurs in thread B.
941+
942+
Failure to recognize changed target object through comparison can result in stale information.
905943
The general concept of solving this problem involves adding more information to make different state distinguishable, and then making a decision on whether to act on the old state or retry with the new state.
906944
If acting on the old state is chosen, then safe memory reclamation should be considered as memory may have already been freed by other threads.
907945
More aggressively, one might consider the programming paradigm where each operation on the target object does not have a side effect on modifying it.
946+
In the later section, we will introduce a different way of implementing atomic \textsc{RMW} operations by using LL/SC instructions. The exclusiveness provided by LL/SC instructions avoids the pitfall introduced by comparison.
947+
948+
To make different state distinguishable, a common solution is adding a version number to be compared as well.
949+
908950

909951
\section{Sequential consistency on weakly-ordered hardware}
910952

examples/simple_aba_example.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#include <stdatomic.h>
2+
#include <stdio.h>
3+
#include <threads.h>
4+
#include <unistd.h>
5+
atomic_int v = 42;
6+
7+
int threadA(void *args)
8+
{
9+
int va;
10+
do {
11+
va = atomic_load(&v);
12+
printf("A: v = %d\n", va);
13+
/* Ensure thread B do something before comparing */
14+
thrd_sleep(&(struct timespec){ .tv_sec = 1 }, NULL);
15+
} while (atomic_compare_exchange_strong(&v, &va, va + 10));
16+
printf("A: v = %d\n", atomic_load(&v));
17+
18+
return 0;
19+
}
20+
21+
int threadB(void *args)
22+
{
23+
atomic_fetch_add(&v, 5);
24+
printf("B: v = %d\n", atomic_load(&v));
25+
atomic_fetch_sub(&v, 5);
26+
printf("B: v = %d\n", atomic_load(&v));
27+
28+
return 0;
29+
}
30+
31+
int main()
32+
{
33+
thrd_t A, B;
34+
thrd_create(&A, threadA, NULL);
35+
thrd_create(&B, threadB, NULL);
36+
/* Ensure all threads complete */
37+
thrd_sleep(&(struct timespec){ .tv_sec = 2 }, NULL);
38+
return 0;
39+
}

0 commit comments

Comments
 (0)