Introduction
The two key primitives used as building blocks for (blocking) concurrent
algorithms are mutexes and condition variables, showing up in C++ as
std::mutex
and std::condition_variable
.
In this post, I'll describe a pair of functions that can be simply described, but whose implementation requires a tricky use of these primitives.
The problem
Suppose we have an array of N
int
s. There are two functions we want to
implement:
-
void modify(size_t index, int value);
This changes the element at positionindex
to equalvalue
. -
void wait_until_equal(size_t index1, size_t index2);
This blocks until the elements at positionsindex1
andindex2
of the array are equal.
The question is: how do we implement modify
and wait_until_equal
?
The rules
Without further requirements, the problem is trivial: just have a single lock
guarding the array, together with a single condition variable that all readers
wait on. But this severely limits concurrency: calls to modify
that operate on
different indices block one another. It's also wasteful of the waiter time;
every waiter must wake up, acquire the lock, and re-check the values of its
indices on every modification to the array, even if the indices it's
interested in weren't modified. This motivates the first rule:
- Calls to one of the functions should not block waiting on a call to another one of the functions unless the two calls have one of their index arguments equal.
We'll relax the problem a little bit, to make it easier, to get a second rule
that deals with races between modify
calls.
- If a call to
wait_until_equal(index1, index2)
occurs, and the elements at indicesindex1
andindex2
become equal only briefly, it's okay for thewait_until_equal
call not to return; it can "miss" situations that don't exist for long enough.
We don't want this rule to be abused though (e.g. with a wait_until_equal
call
that never returns), so we'll add one final rule.
- If the elements at the indices passed to a
wait_until_equal
call become and remain equal, thewait_until_equal
call will eventually return.
Finally, we want to make sure we don't waste too much CPU time; we'll have a no-spinning requirement
- The internals of
wait_until_equal
shouldn't busy-wait by spinning until their conditions become true. Similarly, loops likewhile (!check_condition()) { std::this_thread::sleep_for(std::chrono::milliseconds(15)) }
are cheating. Thewait_until_equal
function shouldn't use CPU time that's not proportional to the number ofmodify
operations that are used on its arguments.
These rules are really just to try to prevent "clever" solutions that dodge the
real concurrency issues that are present. I promise that there's a reasonable
solution to this that doesn't involve waiting forever, calling exit(0)
, or
other trickery. The only concurrency primitives you need are mutexes and
condition variables, and you don't need an unreasonable number of them (e.g.
having N**2
variables is unnecessary).
Discussion
This problem is pretty abstracted, and the API presented to users is unrealistic in several ways:
- There's no way to read the values from the array, only to set them and compare them for equality
- Once you know two values are equal, they won't necessarily remain equal
after the call to
wait_until_equal
returns, so you can't rely on their equality.
But these problems are easy to remedy, once a solution to this simplified problem is found, and solving only the simplification gets to the heart of the matter. There are real-world problems that require techniques similar to this one.
I'll post a solution sometime in the next couple of weeks. I'll keep it in a separate post to avoid spoiling it for anyone reading this one.