Of course you can roll your own proper mutex if you want to get your hands dirty with futexes. IIRC you only need two bits (one waiter bit and one locked bit).
Edit: I think I see what you're getting at, but the point from the article about cache invalidation for the naive implementation still is valid.
That obviously requires you to read the current state upfront, whereas a plan compare exchange from 0 to 1 on 32/64Bit avoids that.
0: CAS_bit_n(void* adr, int n)
1: while(true):
2: old <- read unit(adr)
3: new <- compute-new-value-with-changed-bit-n(old, n)
4: if CAS(adr, old, new):
5: break
6: else
7: continue
Indeed it seems it is correct if you retry. auto old = addr.load(std::memory_order_relaxed);
while(not addr.compare_exchange_weak(old, old|1))
;
In fact you can set old to some random value and it will still convergence to the correct value, but is of course suboptimal.I've always wondered how the wait list was implemented.
Note: I wrote the above from the top of my head, I haven't actually had the need to implement a concurrent hashtable yet.
https://github.com/mirror/x264/blob/d4099dd4c722f52c4f3c1457...
In my own attempts I've been unable to measure the impact of the spinlock count, and took spincount = 40 like in https://trac.webkit.org/browser/webkit/trunk/Source/WTF/wtf/... ...who also took measurement from elsewhere. The default is 4000 on Windows!
I don't argue spinning isn't valuable ; what I say is that it's necessarily never the bottleneck (else your task size really is too small).
After all, if you spend all your time in synchronization, that means you probably need to have bigger tasks, not more clever synchronization primitives.
- Uncontended case + mutex: no syscall, fast path is basically the cost of the memory barrier.
- Uncontended case + spinlock: no syscall, fast path is basically the cost of the memory barrier. Essentially same cost than a mutex, sometimes a tiny bit cheaper but _if your lock isn't contented in the first place it will be very hard to measure, since not being a bottleneck_.
- Contended case + mutex: thread is waiting in the mutex list without taking CPU. Hyper-threading may also give hand immediately to another thread, perhaps lucky enough to run. The OS mutex may have a bit of capped spinning before waiting in the scheduler.
- Contended case + spinlock: Anything goes. CPU gets consumed just for spinning. Wrong thread might get priority. If you are lucky you did put a HLT instruction to mitigate the complete disaster that spinlocks are, perhaps it even does something. Your consolation: no syscall or thread pausing... because they are spinning.
tl;dr: spinlock are worse in the contended case and essentially the same performance in the uncontended case against your typical OS mutex. SPINLOCKS WON'T YIELD THE EXPECTED BENEFITS, USE REGULAR OS MUTEXES.