14.3.2. Semaphores
Semaphores are commonly used in operating systems and concurrent programs where the goal is to manage concurrent access to a pool of resources. When using a semaphore, the goal isn’t who owns what, but how many resources are still available. Semaphores are different from mutexes in several ways:
-
Semaphores need not be in a binary (locked or unlocked) state. A special type of semaphore called a counting semaphore can range in value from 0 to some r, where r is the number of possible resources. Any time a resource is produced, the semaphore is incremented. Any time a resource is being used, the semaphore is decremented. When a counting semaphore has a value of 0, it means that no resources are available, and any other threads that attempt to acquire a resource must wait (e.g., block).
-
Semaphores can be locked by default.
While a mutex and condition variables can simulate the functionality of a semaphore, using a semaphore may be simpler and more efficient in some cases. Semaphores also have the advantage that any thread can unlock the semaphore (in contrast to a mutex, where the calling thread must unlock it).
Semaphores are not part of the Pthreads library, but that does not mean that
you cannot use them. On Linux and macOS systems, semaphore primitives can
be accessed from semaphore.h
, typically located in /usr/include
.
Since there is no standard, the function calls may differ on different
systems. That said, the semaphore library has similar declarations to
those of mutexes:
-
Declare a semaphore (type
sem_t
, e.g.,sem_t semaphore
). -
Initialize a semaphore using
sem_init
(usually inmain
). Thesem_init
function has three parameters: the first is the address of a semaphore, the second is its initial state (locked or unlocked), and the third parameter indicates whether the semaphore should be shared with the threads of a process (e.g., with value 0) or between processes (e.g., with value 1). This is useful because semaphores are commonly used for process synchronization. For example, initializing a semaphore with the callsem_init(&semaphore, 1, 0)
indicates that our semaphore is initially locked (the second parameter is 1), and is to be shared among the threads of a common process (the third parameter is 0). In contrast, mutexes always start out unlocked. It is important to note that in macOS, the equivalent function issem_open
. -
Destroy a semaphore using
sem_destroy
(usually inmain
). This function only takes a pointer to the semaphore (sem_destroy(&semaphore)
). Note that in macOS, the equivalent function may besem_unlink
orsem_close
. -
The
sem_wait
function indicates that a resource is being used, and decrements the semaphore. If the semaphore’s value is greater than 0 (indicating there are still resources available), the function will immediately return, and the thread is allowed to proceed. If the semaphore’s value is already 0, the thread will block until a resource becomes available (i.e., the semaphore has a positive value). A call tosem_wait
typically looks likesem_wait(&semaphore)
. -
The
sem_post
function indicates that a resource is being freed, and increments the semaphore. This function returns immediately. If there is a thread waiting on the semaphore (i.e., the semaphore’s value was previously 0), then the other thread will take ownership of the freed resource. A call tosem_post
looks likesem_post(&semaphore)
.