blob: 09dc9615fcb8ce7add3448c76588c4b70f720734 [file] [log] [blame]
% locking/locking-existence.tex
% mainfile: ../perfbook.tex
% SPDX-License-Identifier: CC-BY-SA-3.0
\section{Lock-Based Existence Guarantees}
\label{sec:locking:Lock-Based Existence Guarantees}
%
\epigraph{Existence precedes and rules essence.}{Jean-Paul Sartre}
A key challenge in parallel programming is to provide
\emph{\IXpl{existence guarantee}}~\cite{Gamsa99},
so that attempts to access a given object can rely on that object
being in existence throughout a given access attempt.
\begin{listing}
\begin{fcvlabel}[ln:locking:Per-Element Locking Without Existence Guarantees]
\begin{VerbatimL}[commandchars=\\\@\$]
int delete(int key)
{
int b;
struct element *p;
b = hashfunction(key);
p = hashtable[b];
if (p == NULL || p->key != key) \lnlbl@chk_first$
return 0;
spin_lock(&p->lock); \lnlbl@acq$
hashtable[b] = NULL; \lnlbl@NULL$
spin_unlock(&p->lock);
kfree(p);
return 1; \lnlbl@return1$
}
\end{VerbatimL}
\end{fcvlabel}
\caption{Per-Element Locking Without Existence Guarantees (Buggy!)}
\label{lst:locking:Per-Element Locking Without Existence Guarantees (Buggy!)}
\end{listing}
In some cases, existence guarantees are implicit:
\begin{enumerate}
\item Global variables and static local variables in the base module
will exist as long as the application is running.
\item Global variables and static local variables in a loaded module
will exist as long as that module remains loaded.
\item A module will remain loaded as long as at least one of its functions
has an active instance.
\item A given function instance's on-stack variables will exist until
that instance returns.
\item If you are executing within a given function or have been
called (directly or indirectly) from that function,
then the given function has an active instance.
\end{enumerate}
These implicit existence guarantees are straightforward, though
bugs involving implicit existence guarantees really can happen.
\QuickQuiz{
How can relying on implicit existence guarantees result in
a bug?
}\QuickQuizAnswer{
Here are some bugs resulting from improper use of implicit
existence guarantees:
\begin{enumerate}
\item A program writes the address of a global variable to
a file, then a later instance of that same program
reads that address and attempts to dereference it.
This can fail due to address-space randomization,
to say nothing of recompilation of the program.
\item A module can record the address of one of its variables
in a pointer located in some other module, then attempt
to dereference that pointer after the module has
been unloaded.
\item A function can record the address of one of its on-stack
variables into a global pointer, which some other
function might attempt to dereference after that function
has returned.
\end{enumerate}
I am sure that you can come up with additional possibilities.
}\QuickQuizEnd
But the more interesting---and troublesome---guarantee involves
heap memory:
A dynamically allocated data structure will exist until it is freed.
The problem to be solved is to synchronize the freeing of the structure
with concurrent accesses to that same structure.
One way to do this is with \emph{explicit guarantees}, such as locking.
If a given structure may only be freed while holding a given lock, then holding
that lock guarantees that structure's existence.
But this guarantee depends on the existence of the lock itself.
One straightforward way to guarantee the lock's existence is to
place the lock in a global variable, but global locking has the disadvantage
of limiting scalability.
One way of providing scalability that improves as the size of the
data structure increases is to place a lock in each element of the
structure.
Unfortunately, putting the lock that is to protect a data element
in the data element itself is subject to subtle \IXpl{race condition},
as shown in
\cref{lst:locking:Per-Element Locking Without Existence Guarantees (Buggy!)}.
\QuickQuiz{
\begin{fcvref}[ln:locking:Per-Element Locking Without Existence Guarantees]
What if the element we need to delete is not the first element
of the list on \clnref{chk_first} of
\cref{lst:locking:Per-Element Locking Without Existence Guarantees (Buggy!)}?
\end{fcvref}
}\QuickQuizAnswer{
This is a very simple hash table with no chaining, so the only
element in a given bucket is the first element.
The reader is invited to adapt this example to a hash table with
full chaining.
}\QuickQuizEnd
\begin{fcvref}[ln:locking:Per-Element Locking Without Existence Guarantees]
To see one of these race conditions, consider the following sequence
of events:
\begin{enumerate}
\item Thread~0 invokes \co{delete(0)}, and reaches \clnref{acq} of
the listing, acquiring the lock.
\item Thread~1 concurrently invokes \co{delete(0)}, reaching
\clnref{acq}, but spins on the lock because Thread~0 holds it.
\item Thread~0 executes \clnrefrange{NULL}{return1}, removing the element from
the hashtable, releasing the lock, and then freeing the
element.
\item Thread~0 continues execution, and allocates memory, getting
the exact block of memory that it just freed.
\item Thread~0 then initializes this block of memory as some
other type of structure.
\item Thread~1's \co{spin_lock()} operation fails due to the
fact that what it believes to be \co{p->lock} is no longer
a spinlock.
\end{enumerate}
Because there is no existence guarantee, the identity of the
data element can change while a thread is attempting to acquire
that element's lock on \clnref{acq}!
\end{fcvref}
\begin{listing}
\begin{fcvlabel}[ln:locking:Per-Element Locking With Lock-Based Existence Guarantees]
\begin{VerbatimL}[commandchars=\\\@\$]
int delete(int key)
{
int b;
struct element *p;
spinlock_t *sp;
b = hashfunction(key);
sp = &locktable[b];
spin_lock(sp); \lnlbl@acq$
p = hashtable[b]; \lnlbl@getp$
if (p == NULL || p->key != key) {
spin_unlock(sp);
return 0;
}
hashtable[b] = NULL;
spin_unlock(sp);
kfree(p);
return 1;
}
\end{VerbatimL}
\end{fcvlabel}
\caption{Per-Element Locking With Lock-Based Existence Guarantees}
\label{lst:locking:Per-Element Locking With Lock-Based Existence Guarantees}
\end{listing}
\begin{fcvref}[ln:locking:Per-Element Locking With Lock-Based Existence Guarantees]
One way to fix this example is to use a hashed set of global locks, so
that each hash bucket has its own lock, as shown in
\cref{lst:locking:Per-Element Locking With Lock-Based Existence Guarantees}.
This approach allows acquiring the proper lock (on \clnref{acq}) before
gaining a pointer to the data element (on \clnref{getp}).
Although this approach works quite well for elements contained in a
single partitionable data structure such as the hash table shown in the
listing, it can be problematic if a given data element can be a member
of multiple hash tables or given more-complex data structures such
as trees or graphs.
Not only can these problems be solved, but the solutions also form
the basis of lock-based software transactional memory
implementations~\cite{Shavit95,DaveDice2006DISC}.
However,
\cref{chp:Deferred Processing}
describes simpler---and faster---ways of providing existence guarantees.
\end{fcvref}
% @@@ Optimized sharded locking from P1726R5, and pointer-zap implications.