Because disabling interrupts does not work on multiple processors, system designers started to
Invent hardware support for locking. The earliest multiprocessor systems, such as the Burroughts
B5000 in the early 1960 ' s, had such support; Today all systems provide this type of support, even
For single CPU systems.
The simple bit of hardware. Understand is known as a test-and-set instruction,
Also known as Atomic Exchange. To understand how Test-and-set works, let's first try to build a
Simple lock without it. In this failed attempt, we use a simple flag variable to denote whether the
Lock is held or not.
In this first attempt, the idea was quite simple:use a simple variale to indicate whether some
Thread has possession of a lock. The first thread that enters the critical section would call Lock (),
Which tests whether the flag is equal to 1 (in this case, it's not), and then sets the flag to 1 to
Indicate that's the thread now holds the lock. When finished and the critical section, the thread
Calls unlock () and clears the flag, thus indicating, the lock is no longer held.
typedef struct __LOCK_T {int flag;} lock_t;void init (lock_t* mutex) { mutex->flag = 0;} void Lock (lock_t* mutex) { while (Mutex->flag = = 1) ; Mutex->flag = 1;} void unlock (lock_t* mutex) { mutex->flag = 0;}
If Another thread happens to call lock () and that first thread are in the critical section, it'll
Simply spin-wait in the If loop for that thread to call unlock () and clear the flag. Once the first
Flag does so, the waiting thread would fall out of the "while loop" set the flag to 1 for itself, and
Proceed into the critical section.
Unfortunately, the code has a problems:one of correctness, and another of performance. The
Correctness problem is simple-to-see once-get used to thinking about concurrent programming
. Imagine the code interleaving; Assume flag = 0 to being.
As can see from this interleaving, with timely (untimely?) interrupts, we can easily produce
A case where both threads set the flag to 1 and both threads is thus able to enter the critical
Section. This behavior was what professionals call "bad"-we had obviously failed to provide the
Most basic requirement:providing mutual exclusion.
The performance problem, which we'll address more later on, are the fact that the the-the-same thread
Waits to acquire a lock this is already held:it endlessly checks the value of flag, a technique
Known as spin-waiting. Spin-waiting wastes time waiting for another thread to release a lock. The
Waste is exceptionally high in a uniprocessor, where the thread that the waiter are waiting for
Cannot even run (at least, until a context switch occurs!) Thus, as we move forward and develop
More sophisticated solutions, we should also consider ways to avoid this kind of waste.
Building A Working Spin Lock
While the idea behind the example above is a good one, it's not possible to implement without
Some support from the hardware. Fortunately, some systems provide an instruction-to-support
The creation of based one this concepty. This + powerful instruction has different
Names-on SPARC, it's load/store unsigned byte instruction (ldstub), whereas on x86, it's the
Atomic Exchange Instruction (XCHG)--but basically does the same thing across platforms, and is
Generally referred to as test-and-set. We define what's the test-and-set instruction does with the
Following C code snippet:
int Testandset (int* old_ptr, int new) { int old = *old_ptr; *old_ptr = new; return old;}
What's the test-and-set instruction does is as follows. It returns the old value pointed to by the PTR,
and simultaneously updates said value to new. The key, of course, is and this sequence of
operations is performed atomically. The reason it is called test-and-set are that it enables
Test the old value (which was returned) while Simultaneouly setting the memory location to
A new value; As it turns out, this slightly more powerful instruction are enough to build a simple
Spin lock, as we now examine in figure 28.3. Or better yet:figure it out first yourself!
Let's make sure we understand what this lock works. Imagine first the case where a thread calls
Lock () and no other thread currently holds the lock; Thus, flag should be 0. When the thread calls
Testandset (flag, 1), the routine would return the old value of flag, which is 0; Thus, the calling
Thread, which is testing the value of flag, would not get caught spinning in the while loop and would
Acquire the lock. The thread would also atomically set the value to 1, thus indicating that the lock
is now held. When the thread was finished with its critical sections, it calls unlock () to set the flag
Back to zero.
typedef struct __LOCK_T { int flag;} void Init (lock_t* lock) { Lock->flag = 0;} void Lock (lock_t* lock) {while (Testandset (&lock->flag, 1) = = 1) ;} void unlock (lock_t* lock) { Lock->flag = 0;}
The second case we can imagine arises when one thread already have the lock held (i.e., flag is 1).
In this case, the this thread would call lock () and then call Testandset (flag, 1) as well. This time,
() would return the old value at flag, which was 1 (because the lock is held) and while Simultaneouly
Setting it to 1 again. As long as the lock is held by another thread, Testandset () would repeatedly
Return 1, and thus this thread would spin and spin until the lock is finally released. When the flag is
Finally set to 0 by some other thread, this thread would call Testandset () again, which'll now
return 0 While atomically setting the value to 1 and thus acquire the lock and enter the critical
Section.
By making both the test of the old lock value and set of the new value a single atomic operation,
We ensure that only one thread acquires the lock. And Thst ' s how to build a working mutual
Exclusion primitive!
Also now understand what this type of lock is usually referred to as a spin lock. It is the
Simplest type of lock to build, and simply spins using CPU cycles, until the lock becomes available.
To work corectly in a single processor, it requires a preemptive scheduler (i.e., one that would
Interrupt a thread via a timer, in order to run a different thread, from time to time). Without
preemption, spin locks don ' t make much sense to a single CPU, as a thread spinning on a CPU
would never relinquish it.
Tips:think about Concurrent as malicious Scheduler
From this example, you might get a sense of the the approach you need to take to understand
Concurrent execution. What you should try-to-do-pretend-a malicious scheduler, one
That interrupts threads at the most inopportune of times in order to foil their feeble attempts at
Building synchronization Promitives. What a mean scheduler you are! Although the exact sequence
Of interrupts may improbable, it's possible, and that's all we need to demonstrate that a
Particular approach does not work. It can useful to think maliciouly! (at least, sometimes.)
Operating system:three Easy Pieces---locks:test and Set (Note)