| % locking-existence.tex |
| |
| \section{Lock-Based Existence Guarantees} |
| \label{sec:locking:Lock-Based Existence Guarantees} |
| |
| \begin{figure}[tbp] |
| { \scriptsize |
| \begin{verbatim} |
| 1 int delete(int key) |
| 2 { |
| 3 int b; |
| 4 struct element *p; |
| 5 |
| 6 b = hashfunction(key); |
| 7 p = hashtable[b]; |
| 8 if (p == NULL || p->key != key) |
| 9 return 0; |
| 10 spin_lock(&p->lock); |
| 11 hashtable[b] = NULL; |
| 12 spin_unlock(&p->lock); |
| 13 kfree(p); |
| 14 return 1; |
| 15 } |
| \end{verbatim} |
| } |
| \caption{Per-Element Locking Without Existence Guarantees} |
| \label{fig:locking:Per-Element Locking Without Existence Guarantees} |
| \end{figure} |
| |
| A key challenge in parallel programming is to provide |
| \emph{existence guarantees}~\cite{Gamsa99}, |
| so that attempts to access a given object can rely on that object |
| being in existence throughout a given access attempt. |
| 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 race conditions, |
| as shown in |
| Figure~\ref{fig:locking:Per-Element Locking Without Existence Guarantees}. |
| |
| \QuickQuiz{} |
| What if the element we need to delete is not the first element |
| of the list on line~8 of |
| Figure~\ref{fig:locking:Per-Element Locking Without Existence Guarantees}? |
| \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 |
| |
| \QuickQuiz{} |
| What race condition can occur in |
| Figure~\ref{fig:locking:Per-Element Locking Without Existence Guarantees}? |
| \QuickQuizAnswer{ |
| Consider the following sequence of events: |
| \begin{enumerate} |
| \item Thread~0 invokes \co{delete(0)}, and reaches line~10 of |
| the figure, acquiring the lock. |
| \item Thread~1 concurrently invokes \co{delete(0)}, reaching |
| line~10, but spins on the lock because Thread~0 holds it. |
| \item Thread~0 executes lines~11-14, 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 line~10! |
| } \QuickQuizEnd |
| |
| \begin{figure}[tbp] |
| { \scriptsize |
| \begin{verbatim} |
| 1 int delete(int key) |
| 2 { |
| 3 int b; |
| 4 struct element *p; |
| 5 spinlock_t *sp; |
| 6 |
| 7 b = hashfunction(key); |
| 8 sp = &locktable[b]; |
| 9 spin_lock(sp); |
| 10 p = hashtable[b]; |
| 11 if (p == NULL || p->key != key) { |
| 12 spin_unlock(sp); |
| 13 return 0; |
| 14 } |
| 15 hashtable[b] = NULL; |
| 16 spin_unlock(sp); |
| 17 kfree(p); |
| 18 return 1; |
| 19 } |
| \end{verbatim} |
| } |
| \caption{Per-Element Locking With Lock-Based Existence Guarantees} |
| \label{fig:locking:Per-Element Locking With Lock-Based Existence Guarantees} |
| \end{figure} |
| |
| 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 |
| Figure~\ref{fig:locking:Per-Element Locking With Lock-Based Existence Guarantees}. |
| This approach allows acquiring the proper lock (on line~9) before |
| gaining a pointer to the data element (on line~10). |
| Although this approach works quite well for elements contained in a |
| single partitionable data structure such as the hash table shown in the |
| figure, 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. |
| These problems can be solved, in fact, such solutions form the basis |
| of lock-based software transactional memory |
| implementations~\cite{Shavit95,DaveDice2006DISC}. |
| However, |
| Chapter~\ref{chp:Deferred Processing} |
| describes simpler---and faster---ways of providing existence guarantees. |