The Little Book of Semaphores (2016) [pdf]
greenteapress.com
greenteapress.com
A particularly tricky problem is clean program termination when waiting on semaphores. It is much easier to add while(!quit && not_satisfied) cond_wait(lock, is_satisfied); than doing the same with semaphores. You need to add extra synchronization for this case with semaphores. Every call to cond_wait usually needs a check for a "quit" flag.
Semaphores are still useful in other kinds of asynchronous processes such as CPU to GPU synchronization or keeping trains on railways from colliding.
If you encounter a semaphore in multi threaded code that's been written recently, assume it is incorrect. That's the case in the past half a dozen cases where I came across one in the codebase I work on. The most common bug was the program hanging on exit. Replacing them with mutex and condition variables (plural!) was easy, the solution was much simpler to understand and it was correct and the programs now terminated cleanly when exit was requested.
Semaphores should really be removed from the OS class curriculum. Maybe leave them in the advanced parallel/concurrent programming course because they're easy to reason about in formal proofs. But in multi threaded programming, they're almost always the wrong tool for the job.
I see no reason to use semaphores in multithreaded programming because of this. They have no advantages over mutex and condition but are much more limited.
Agreed. An interesting exercise is to try the reverse: implementing a condition variable with a semaphore. It's not trivial to do correctly.
I teach OS, and I still cover semaphores, but I always tell the class that I don't think they are very useful. I spend much more time on condition variables.
Overall I am quite disappointed at the level of education with parallelism and concurrency now that we're 15 years into the multi-core era, let alone in the years past. I'm constantly having issues with coworkers who are quite clueless in the topic, doing futile attempts in taking a textbook example and applying that to a real world problem (usually involving semaphores or trying to use atomics because "locks are hard").
It's equally frustrating to hear the "don't use locks, do CSP/message passing/concurrent collections/lock-free queues instead" cargo cult. Yes, that works for a class of problems but you still need locks to implement message passing (no excuse to not learn locking) and getting simple atomicity invariants right is as hard or harder than with locks.
It takes discipline and hard work to get multithreaded programming right but there are plenty of silver bullets and snake oil being pushed.
Programs should be optimized for the uncontended case where a mutex and semaphore are pretty similar.
Some textbook toy problems are theoretically faster with semaphores but that's not really the case for practical problems.
The overhead of synchronization primitives isn't really a concern in practice.
In that case, using semaphores or other "exotic" synchronization primitives (my favorite are Windows's manual-reset events) can help eliminate contention.
Problems with exiting threads are real. An often sufficient solution is to make the semaphore (or event) waiters "detached" and not care about them, while ensuring that the producers don't exit until their work has been completed.
As with other advanced multithreaded programming techniques, discipline is key, together with noticing, documenting and applying recurrent idioms.
I would think that using a lock free atomic solution would win there, assuming the contention is high and consistent. Even if the semaphore is marginally faster than a mutex for the uncontended case, both will call into kernel for the contended case. That's 25 ns for uncontended and 150 us for contended. Ie. you could spin about 6000 times on the atomic operation and still win.
I agree with you that discipline and applying best practices without compromise is the way to solve multithreaded problems. Testing, verification and tooling need to be top notch or the result is something that works most of the time.
Test condition (e.g. queue not empty)
If false,
Reset event
Test condition again
If false,
Wait on event
The event stays set most of the time, which helps reducing cacheline bounces between the producers even if you don't have contention; so even if you use lock free atomic operations with optimistic spinning for the busy case, (event) semaphores can then still be used when the worker goes to sleep.The event itself can be implemented as a mutex/condvar pair, alternatively, but that's pointless if you document what you are doing and do it consistently.
Sure I could use a mutex and a counter plus comparison and busy wait... but why? As far as I'm concerned, a semaphore is just a more extendable mutex.
A semaphore is just a crippled condition variable that had only a single integer as a state. It sort-of works but isn't very practical in real world applications.
Lock-free programming with atomics is even more difficult than reasoning about code with locks. It is best left to very specialized cases where maximum throughput is necessary and contention is high. Most practical use cases don't benefit from that.
The key is minimizing the time the locks are held. That drives down contention and keeps performance high.
If you just use a disruptor implementation then there's no need to reason about anything. It's good to go and performant by default.
If contention is extremely high and throughput is paramount, then a lockless solution might work. E.g. audio processing or network stacks might have this kind of usage pattern.
But in my experience, most queues are empty most of the time. That's when you want a lock so the consumer thread can sleep instead of busy wait.
(This is why I would generally advocate for a message-passing style for almost everything, but obviously that's not quite the context of this thread. My reasoning is that you can always start off with message passing and if you discover that that's your bottleneck, you can always transition to a faster alternative[1]. Plus, you could use a lock-free message passing solution, e.g. Aeron.)
[1] Plus, by using message passing you've already got pretty good separation of logic.
A futex is just a specific implementation of a mutex though right? Or am I reading your comment wrong? I am curious where the 25 ns comes from.
Also isn't what makes a futex so fast is that it's done in user space - hence the name?
That's essentially the cost of an atomic operation in the CPU interconnect.
A futex is a way to implement a mutex so that the uncontended case is just a spinlock acquire, and the contended case falls back to calling the kernel.
[0] https://people.eecs.berkeley.edu/~rcs/research/interactive_l...
Should look something like this:
for(;;){
if(atomic_swap(&thefutex, 1) == 0)
break; // we own the lock now
// slow path, go to sleep until awoken
syscall(SYS_FUTEX, FUTEX_WAIT, ...);
}1. Concurrent access to a variable out of a critical section is qualified as not being dangerous. This actually can't be decided without knowing about the memory model you are considering, and both in the general case and in practical implementations (C++) this is UB.
2. This sentence is maybe only trying to be funny, but discouraging formal proofs in 2016 can very well end up borderline criminal (depending on the application): "The only alternative is to examine the code carefully and “prove” that it is correct. I put “prove” in quotation marks because I don’t mean, necessarily, that you have to write a formal proof (although there are zealots who encourage such lunacy)."
Especially if this is stated right after a rather simple algorithm.
I can absolutely not recommend a book about a critical topic, which contains that kind of defects.
Huh. I had heard of nasal demons before but I had never seen this abbreviation "UB"
TIL, UB and IB
https://stackoverflow.com/questions/2766731/what-exactly-do-...
I think your definition of formal proofs is a bit different from Alan Downey's (who incidentally has also written an entire book on Complexity and the Philosophy of science[1]). Providing a formal proof for even a 'simple' algorithm is incredibly wasteful in the context of the example in which the statement is made. Perhaps you should read the book before calling out it's 'defects'.
[1] http://greenteapress.com/complexity/index.html
More of Alan's books at: http://greenteapress.com/
IMO the problems in actual software are almost always between the keyboard and the chair; one can write junk using any sync primitives. My 2c.