CS 321 Spring 2017 > Notes for February 8, 2017
CS 321 Spring 2017
Notes
for February 8, 2017
Shared-Memory IPC (cont’d)
Hardware Support for Mutual Exclusion
Hardware support for mutual exclusion involves special processor instructions that perform atomic operations. One simple hypothetical instruction is Test & Set Lock (TSL). This instruction would be given an register and a memory location (the location of a lock variable). It reads the contents of the memory location into the register and stores some fixed value into the memory location. All this is done in an atomic manner.
The idea is that TSL would be an assembly language instruction. But we can think of it as follows (assuming the above fixed value is zero).
[C++]
const int FIXED_VALUE = 0; int TSL(int & memloc) { // The following two lines need to be atomic int save_memloc = memloc; memloc = FIXED_VALUE; return save_memloc; }
If we have a TSL available, then we can use it to make our
original incorrect locking code
behave correctly.
Our unlock
function is unchanged.
Our lock
function can be rewritten
as follows.
[C++]
void lock() // Call when entering critical region { while (true) { int old_lock = TSL(_lock_var); if (old_lock == 1) break; } }
The x86 architecture does not have a TSL instruction, but it does have something just a bit more general: XCHG. This does an atomic exchange of the values of a register and a memory location. Moving a value of 0 into the register beforehand results in the same effect that the above TSL would have. See also the x86 CMPXCHG instruction, which does a fancier atomic operation.
Problems When Sharing Resources
The Priority-Inversion Problem happens when we have a high-priority process/thread busy-waiting on a lower-priority process/thread. We need to give time to the lower-priority process so that it can finish and the higher-priority one can do its work. But standard scheduling algorithms will give more time to the higher-priority process. This is another reason to avoid busy waiting.
The Producer-Consumer Problem occurs when data is being transfered from a producer process to a consumer process. To avoid busy-waiting, we might have the consumer process check if there is data available, and if not, sleep. Now suppose there is an interrupt between a check—which says there is no data—and the sleep. Then the producer can send data, and wake the consumer ... which is already awake and preparing to sleep. So the producer sends data, and the consumer responds by sleeping. This is another situation when atomic operations are needed. The Produce-Consumer Problem is a special case of the Lost-Wakeup Problem.
Kernel-Provided Mechanisms for Handling Shared Resources
A useful primitive for managing concurrent processes/threads is a semaphore. This is a nonnegative integer with two associated atomic operations: down and up.
A down operation attempts to decrement the value. If it is nonzero, then this is successful; otherwise, the process goes to sleep (i.e., it is blocked) and is added to a list of processes sleeping on the semaphore. An up operation increments the value. If the value was zero and at least one process was sleeping on the semaphore, then one such processes is awakened, to complete its decrement.
Note: The down operation was originally called P. It is also known as acquire or wait. The up operation was originally called V. It is also known as release, post, or signal.
A semaphore provides a ready-made solution to the Producer-Consumer problem. The value of the semaphore could be the number of items available in a buffer. The producer writes and then does an up; the consumer does a down and then reads. The atomic nature of the down operation means that there can be no interrupt between checking the value and sleeping; thus there is never a lost wakeup.
We can also use a semaphore to manage access to a critical region: do a down before entering the region and an up after exiting. The initial (and maximum) value of the semaphore is thus the maximum number of processes allowed in the critical region at once.
This suggests a useful special case: a semaphore with maximum value one. Such a zero/one value with down & up operations is a binary semaphore. A binary semaphore makes it straightforward to implement a mutex lock: to lock, do a down; to unlock, do an up.
The various kinds of semaphores will typically be provided by the kernel. Other concurrency-management abstractions (locks, monitors, etc.) can be built on top of them.
One rather different concurrency-managment abstraction is a barrier: a serial bottleneck used as a synchronization mechanism. A serial bottleneck is a part of the code in which all processes must wait for a single process. Serial bottlenecks can be a problem, since they tend to limit the efficiency of multiprocessor architectures; but when we use a barrier, we introduce a bottleneck deliberately.
See
write_mutex4.cpp
(in the Git repository)
for a program that implements a mutex using
a POSIX semaphore.