A counting semaphore tracks available resources. A thread calls wait() to acquire one and signal() to release one. When the counter is zero, wait() blocks until a resource becomes available.
A semaphore-controlled parking lot with three spaces
Semaphore S manages three parking spaces
Press Car arrives repeatedly to fill the lot. Click a parked car to make it leave and call signal().
wait() and signal()
If S is positive, wait() decrements it and proceeds. If S is zero, the caller blocks. signal() releases a resource and wakes a waiting thread, if any. The awakened thread immediately takes the released resource, so S remains zero when cars are queued.
A five-slot producer–consumer buffer coordinated by semaphores
Producer–consumer: empty and full track five buffer slots
Insert and remove individual items, or start automatic execution. If production is faster, the full buffer blocks the producer. If consumption is faster, the empty buffer blocks the consumer.
The producer–consumer problem
A producer needs an empty slot; a consumer needs a filled slot. The empty and full semaphores enforce these conditions. A mutex also protects modifications to the buffer itself.
Producer: wait(empty), lock(m), insert item, unlock(m), signal(full). Consumer: wait(full), lock(m), remove item, unlock(m), signal(empty).
How is this different from a mutex?
A mutex must be unlocked by its owner. A semaphore has no owner, so another thread can signal it. Besides counting resources, semaphores can notify a task that an event occurred. For example, a FreeRTOS interrupt handler can use xSemaphoreGiveFromISR() to wake a task.