Using Queues: Sleeping Instead of Spinning
Explore how to implement efficient concurrency locks by using queues combined with operating system calls such as park and unpark to put threads to sleep instead of spinning. Understand how this approach controls lock acquisition order, reduces CPU waste, and avoids common problems like starvation, waiting race conditions, and priority inversion in multithreaded environments.
We'll cover the following...
The real problem with the previous approaches is that they leave too much to chance. The scheduler determines which thread runs next; if the scheduler makes a bad choice, a thread runs that must either spin waiting for the lock (our first approach) or yield the CPU immediately (our second approach). Either way, there is potential for waste and no prevention of starvation.
Thus, we must explicitly exert some control over which thread next gets to acquire the lock after the current holder releases it. To do this, we will need a little more OS support, as well as a queue to keep track of which threads are waiting to acquire the lock.
park() and unpark()
For simplicity, we will use the support provided by Solaris, in terms of two calls: park() to put a calling thread to sleep and unpark(threadID) to wake a particular thread as designated by threadID. These two routines can be used in tandem to build a lock that puts a caller to sleep if it tries to acquire a held lock and wakes it when the lock is free. Let’s look at the code excerpt below to understand one possible use of such primitives.
We do a couple of interesting things in this example. First, we combine the old test-and-set idea with an explicit queue of lock waiters to make a more efficient lock. Second, we use a queue to help control who gets the lock next and thus avoid starvation.
You might notice how the guard is used in this example, basically, as a spin-lock around the flag and queue manipulations, the lock is using. ...