Semaphores are surprisingly versatile
preshing.com
preshing.com
Also, note that the examples on this page depend on atomic operations in addition to semaphores, not semaphores alone. If you only use semaphores, you must also use them to protect the accesses that are required to be atomic, which complicates the code.
Let's look at LightWeightMutex. This object is so light weight that we can't even ask it whether it is currently locked, and who the owner is. These features are important for error detection and debugging: real-world requirements that actual mutex implementations satisfy.
Another comment; I find the following completely pointless:
void lock()
{
if (m_contention.fetch_add(1, std::memory_order_acquire) > 0)
{
m_semaphore.wait();
}
}
A semaphore is already supposed to implement counting. What we have here is an implementation of a counting semaphore, using a semaphore.That is to say, to implement a light weight lock using a semaphore, we actually need only this:
void lock() { m_semaphore.wait(); }
The semaphore already has a built in atomic variable equivalent to the m_contention. The atomic increment and test wrapped around this is redundant, and has nothing to do with implementing a lock with a semaphore.This could be repaired by reframing the example as "look, slow semaphores, together with an atomic increment/test operation provided by the processor, can be used to implement faster semaphores!"
This is actually a useful case that tends to be overlooked by "primitives X, Y, and Z out of semaphores" tutorials.
As mentioned in the article, most mutex implementations already use this trick. So you can just use std::mutex, and things are fine.
In the past, though, runtime environments weren't so well-developed, so there definitely [was a point](http://www.haiku-os.org/legacy-docs/benewsletter/Issue1-26.h...).
I see now why your original comment was a bit inflammatory. I should have been more clear in the post that by "lightweight", I meant exactly that: "fast path in user space". I guess not everyone shares this vocabulary. I'll improve the post.
You're right that this lightweight mutex is a semaphore, of course. But not every semaphore is a lightweight mutex. So the technique isn't pointless.
I suppose it is a fair point that the thing you use to enter the kernel doesn't have to be itself a semaphore.
However, as you say, some end up being hideously complicated. Some are "closer to the metal" than others - both in terms of the actual instruction set implementation, and kernel mechanisms used.
I've never been a fan of semaphores, because when used with count>1, they tend to be duplicating some other information in the system, which is easy to get out of sync. When used with just count 0 or 1, it's more intuitive (and closer to underlying implementation) to just use a mutex. And as you point out, the various mutex incarnations usually have an easier time of tracking ownership and debugging, by their nature.
Not to mention that any partial recursive function can be computed by a machine which can move along an unlimited tape, reading or writing a symbol, according to a handful of rules. :)
> And as you point out, the various mutex incarnations usually have an easier time of tracking ownership and debugging
Ownership, whether or not the owning thread died, deadlock detection, recursion detection ... priority inversion handling like priority inheritance ...
By the way, as the author points out, we called this construct "Benaphores" in BeOS. Actually, semaphores were the primary kernel synchronization primitive in BeOS; there were no mutexes. Even sleep was implemented as a wait on a semaphore with a timeout. It made the kernel code cleaner because there was only one code path that unblocked threads. From the user space perspective, I found it to be pretty easy to use, especially since BeOS could acquire with a count greater than one. You could pretty easily to make a reader-writer lock with one semaphore (the writer acquired the max count)
The downside, as others pointed out, was that there was no way to do priority inheritance.
Implementation:
https://github.com/scraperwiki/s4log/blob/master/semaphore.g...
Use:
https://github.com/scraperwiki/s4log/blob/2714f9553880121ad6...
Useful for:
1. Writing a web handler which may use some CPU time and you don't want too many of them in flight at once (to prevent them from all slowing down to mud). (Best combined with something to abort if we had to wait to long before we could even do computation)
2. Execute a parallel upload to S3, but avoid doing too many at once (again, because you don't want very many of them contending on the network connection such that none proceed).
http://cellperformance.beyond3d.com/articles/2009/08/roundup...