Conceptually, a semaphore is equivalent to a mutex, condition variable, and integer counter protected by the mutex. Under this analogy, posting a semaphore is equivalent to locking the mutex, incrementing the counter, signaling the condition variable, and unlocking the mutex. Even if there is no waiter, state is still modified.
Under this analogy, waiters for the semaphore are doing the equivalent of:
- Lock mutex.
- While count is non-positive, wait on condition variable.
- Decrement count.
- Unlock mutex.
Of course if you're talking about the specific case of POSIX, the analogy does not correspond fully to the reality, because semaphores have additional async-signal-safety properties that preclude implementing them using a mutex/condvar/count triple.