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, which is 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 in main). The sem_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 call sem_init(&semaphore, 1, 0) indicates that our semaphore is initially locked (the second parameter is 1), and is to be shared amongst 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 is sem_open.

  • Destroy a semaphore using sem_destroy (usually in main). This function only takes a pointer to the semaphore (sem_destroy(&semaphore)). Note that in macOS, the equivalent function may be sem_unlink or sem_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 to sem_wait typically looks like sem_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 resource freed. A call to sem_post looks like sem_post(&semaphore).