Lecture 6: Semaphores

Commandments of synchronization

Please read the 12 commandments of synchronization (linked from the schedule). They will help you use the synchronization primitives correctly.

Today we mentioned the following:

Semaphore interface

Semaphore is a data structure that encapsulates an integer. From the user's perspective, the integer is never allowed to become negative; attempting to decrement will block the running thread until another thread increments the count.

Semaphores support the following interface: - initialize the semaphore to an initial value - V: increment the semaphore, also called release, or signal. - P: block until the semaphore has a positive value, then decrement it. also called acquire or wait.

Some semaphore implementations allow you to perform other operations. You should avoid using anything other than P and V. For example, python provides the ability to acquire without blocking; other libraries provide the ability to read the internal value of a semaphore. Using these operations can easily lead you to write buggy code. Stick to P and V.

Semaphore idioms

Semaphores are a low level primitive; there are a few common patterns for their use.

binary semaphores / critical sections

Semaphores can be used to implement a critical section: a block of code that only one thread should execute at once.

The semaphore will have the value 0 (indicating a thread is inside the critical section) or 1 (indicating that no thread is running the critical section). A semaphore that is intended to only have a value of one or zero is called a binary semaphore.

semaphore implementation of critical sections
Shared state:
lock = new Semaphore(1)
Thread one code:
lock.P()
# code for critical section
# for example:
# if not milk: buy_milk(); milk = True
lock.V()
Thread two code (same)

counting semaphores / resource acquisition

Semaphores can also be used to control access to a pool of shared resources. The value of the semaphore indicates how many resources are available; a thread can P the semaphore to acquire a resource, use it, then V on the semaphore to return the resource to the pool.

The initial value of the semaphore should be the total number of available resources.

signalling semaphores / producer consumer

Semaphores are often used to implement a signalling pattern, where one thread wants to wait for an event to occur, which another thread causes. For example, we may use a signalling semaphore to implement user input:

signal semaphore example
Shared state:
keyboard_signal = new Semaphore(0)
read syscall handler
# wait for input
keyboard_signal.P()
# input is available
return from syscall
keyboard interrupt handler:
# signal input available
keyboard_signal.V()

per-thread semaphores / producer consumer

In the above example, we didn't transmit the keypress back to the calling thread. To do so, we would need someplace to put the data, and we would need to ensure that the thread that awakens from P() is the same as the thread that receives the data. A common pattern is to create a separate signalling semaphore for each thread, and put them into a data structure.

signal semaphore example with per-thread signals
Shared state:
class Entry:
  - signal : semaphore, initialized to 0
  - data   : initialized to None
         # only updated by producer
         # this.signal is signalled when available

# waiting is protected by waiting_lock
waiting      = new empty Queue of Entry objects
waiting_lock = new Semaphore(1)
consumer:
# create an empty entry
# to be filled by producer
entry = new Entry()

# enqueue the entry
waiting_lock.P()
waiting.enqueue(entry)
waiting_lock.V()

# wait for the entry to
# be filled
entry.signal.P()
return entry.data
producer:
# select waiting entry
waiting_lock.P()
if waiting is empty:
  # ignore input
  waiting_lock.V()
else:
  # deliver input
  entry = waiting.dequeue()
  waiting_lock.V()

  entry.data = input
  entry.signal.V()

Semaphore implementation

Semaphores can be implemented inside the operating system by interfacing with the process state and scheduling queues: a thread that is blocked on a semaphore is moved from running to waiting (a semaphore-specific waiting queue). A spin lock is used to protect the internal state of the semaphore (not shown: the global ready queue needs to be protected as well).

Semaphore implementation (inside OS)
Shared state:
# test and set lock
lock = 0

# queue of TCBs
# protected by "lock"
waiting = new Queue()

# semaphore state
# protected by lock
count
P code:
while test_and_set(lock):
    yield to other threads
    (place current TCB on ready queue)

if count == 0:
    waiting.enqueue(this thread's TCB)
    lock = 0
    stop this thread
    (do not put TCB on ready queue)

    # NOTE: there is a race condition if you context switch between the above
    # two lines.  You really need a atomic deschedule and release operation

else
    count--
    lock = 0
V code:
while test_and_set(lock):
    yield to other threads

if count == 0 and waiting is not empty:
    t = waiting.dequeue()
    place t on ready queue
else:
    count++

lock = 0