Chapter 11: Low-level synchronization: algorithms
11.2 An example of semaphores in system design: the THE system
(THE = Technische Hogeschool Eindhoven, Dijkstra leverde hier baanbrekend werk met concept van semafoor)
The lowest level (0), creates virtual processors. Above this, processes exist and may communicate using a semaphore mechanism. The rest of the system may be written using these concurrency constructs. All interrupts enter at this level, and all but the clock interrupt are handled at higher levels. At level 1, one process provides a one-level virtual store. It synchronizes with drum interrupts and with requests for store from higher-level processes. At level 2, one process provides virtual consoles for higher-level processes and syncs with their requests. It also syncs with console interrupts and the memory manager process. At level 3, a separate process manages each physical device. Each process syncs with its device’s interrupts, with the memory and console manager processes, and with higher-level processes over I/O requests. At level 4, the highest, reside 5 user processes. In all cases where data is passed, producer-consumer style message buffering must be used, controlled by semaphores.
11.3 The producer-consumer, bounded buffer problem
11.3.2 Definition of a cyclic or bounded buffer
circular array
* the index values ranging from 0 to N-1
* some slots are occupied by data items (indicated by ‘data’); others are empty (indicated by shading)
* two cursors (variables holding a valid index value) named ‘in’ and ‘out’, the purposes of which are, respectively, to indicate the first available empty slot and the next data item that can be taken from the buffer.
11.3.3 Algorithm for a single producer and single consumer
figure gives high-level view of a solution to the single producer, single consumer problem. The problem would be programmed in practice in a modular style by defining a buffer class. (hoe lossen we bounded buffer probleem op met semaforen?)
two semaphores, items and spaces
note that for a single producer and a single consumer the buffer is not accessed under mutual exclusion since they are accessing different parts of the buffer (using different cursors). Sync of the processes ensures that they block before attempting to access the same slot in the buffer.(2 semaforen die gedeeltelijk elkaars spiegelbeeld zijn => counting semaphore; maar er wordt wel voor synchronisatie links en rechts gezorgd. Geen kritische regio (want 1 producer, 1 consumer) dus geen binaire semafoor nodig voor wederzijdse uitsluiting => verandert bij overgang naar meerdere producers, meerdere consumers)
11.3.4 Algorithm for more than one producer or consumer
the many producers must access the buffer under mutual exclusion as they are using the same cursor to insert items.
enforce exclusive access to the buffer between consumers
one producer and one consumer could access the buffer concurrently, provided producers and consumers use different guard semaphores.
11.4 Safety and liveness properties
11.5 The multiple readers, single writer problem
figure 1: basic structure of code which must be executed by reader processes and writer processes. To acquire or release the resource, shared counts must be accessed under mutual exclusion. The fact that writers must write under mutual exclusion is indicated.
figure 2: gives the algorithm in detail. Priority is given to waiting writers over waiting readers, since active readers only go on to become reading readers if there are no active writers. The various counts are accessed under mutual exclusion, protected by the semaphore CGUARD. Processes wait to read or write on semaphores R or W respectively. R and W are given ‘wake-up-waiting’ signals in the acquire procedures if there are no active writers (for R) or no running readers (for W). releasing the resource may involve sending sync signals to waiting readers and writers; the last reading reader wakes up any waiting writers, the last active writer wakes up waiting readers.
(readers-writers probleem: meerdere lezers kunnen gegevens terzelfder tijd lezen, hinderen elkaar niet; schrijver zorgt voor probleem: er mag niemand lezen tijdens het schrijven + write moet exclusief zijn (geen andere schrijvers), assumptie: schrijvers krijgen voorrang)
11.6 Limitations of semaphores
operations do not allow a test for busy without a commitment to blocking
11.7 Eventcounts and sequencers
11.7.1 Use of eventcounts for sync
a process specifies which occurrence of an event it is waiting for, rather than just the next event of that type. We therefore introduce a local count I within the process for this purpose.
11.7.2 Use of a sequencer to enforce mutual exclusion
process competing with others to use a resource protected by the eventcount GUARD. The process first acquires a ticket, stored in its local variable myturn. It then attempts to enter its critical region by executing GUARD.await(myturn).
the order of execution of critical regions is determined by the values returned by turns.ticket() (low => high)
(minstens even complex als semaforen en lost eiglk niets op; await(value): niet meer wachten tot iets vrijkomt, maar op bepaalde waarde; advance : doet value +1
zodat volgend process verder kan)
11.7.3 Producer-consumer, bounded buffer with eventcounts and sequencers
producer and consumer may access the buffer at the same time since the solution to the sync problem ensures that the consumer does not read the ith item until the producer has written it and the producer does not write into the ith slot until the consumer has read the i-Nth item from it.
figure1: eventcounts ‘in’ and ‘out’ count the items put in by the producer and taken out by the consumer. When there are multiple producers and consumers, a sequencer ‘tp’ must be used to order the accesses to the buffer of producers and one ‘tc’ for that of consumers. Figure 2 outlines the solution.
11.8 POSIX threads
11.8.2 Synchronization
Mutexes
a mutex (mutual exclusion) is an object that multiple threads use to ensure the integrity of a shared resource that they access by allowing only one thread to access it at a time. Mutex has two states: locked and unlocked. For each piece of shared data, all threads accessing that data must use the same mutex: each thread locks the mutex before it accesses the shared data and unlocks the mutex when it has finished accessing that data. Each mutext must be created by pthread_mutex_init
* fast mutex: locked exactly once by a thread. If a thread attempts to lock the mutex again without first unlocking it, the thread will wait for itself to release the lock and will deadlock.
* recursive mutex: can be locked more than once by a given thread without causing a deadlock. Useful if a thread needs exclusive access to a piece of data, and it needs to call another routine that needs exclusive access to the data.
* non-recursive mutex: locked exactly once by a thread. Error if any thread tries to lock already locked mutex. Useful during development and debugging.
mutex sync operations:
* pthread_mutex_lock: if mutex is locked, the thread waits for the mutex to become available
* pthread_mutex_trylock: this returns immediately with a boolean value indicating whether or not it was able to lock the mutex
*pthread_mutex_unlock: when a thread has finished accessing a piece of shared data, it unlocks the associated mutex by calling pthread_mutex_unlock
Condition variables
a condition variable allows a thread to block its own execution until some shared data reaches a particular state. A condition variable is a sync object used in conjunction with a mutex. A condition variable allows threads to wait for that data to enter a defined state.
a condition variable is used for tasks with coarse granularity; a thread can wait on a condition variable for long periods. A mutex is used for sync with fine granularity and should be held only for short periods of time.
Waiting on the condition variable automatically unlocks the mutex.