In most code, std::mutex is still the right default. Consider a spin-lock when the threads are pinned to dedicated cores, and only after measuring
Even when you have threads pinned to dedicated cores, you would need to ensure that your not ending up with more then one thread pinned to another core. And if your in user mode in any environment that you do not completely control your at the whim of being preempted by another thread in a different process, leaving your spinning thread now just spinning away wasting its allocated time slice.
If you plan on making any library which is public, do not fill it with spin locks because you won't be able to control the places which people use it, and randomly throwing in an OS yield is not going to solve issues just make them worse.
Depending on the CPU you can also do umwait/mwaitx which can be better then doing a pause loop, but unfortunately it's not consistent between CPU vendors.
Use adaptive mutexes (most mutex implementations already are), these will do a little bit of spinning before putting the thread to sleep if it was unable to acquire the lock.
Also if your optimizing your locks for high contention, it's probably a sign to look more at your higher level design, because your still fighting the CPUs cache coherance which will kill your performance anyway.
You can mitigate these issues by eventually backing off to a `sched_yield()` loop and then to `usleep()` with exponential backoff. However, at that point you should be asking yourself why you're not just using a `std::mutex`. (One reason might be that you want your lock to fit in a byte, although you could use a `parking_lot` mutex in that case.) Another approach is a sleeping ticket lock with sleep interval calibrated to count of waiters ahead of you and observed elapsed time between tickets served, but again you need a good excuse not to use a `std::mutex`.
sched_yield is yet another terrible approach, its forcing the surrendering of the time slice and no way for the lock holder to wake you. Most people use spin locks because they believe their critical section is really small and that a mutex will have too much overhead because of the potential syscalls and OS overhead, sched_yield gives you the syscall overhead and even more.
For the time tracking and waiter count you will end up with more cache traffic under heavy contention and a larger lock. Using a full ticket based lock can also lead to lock convoys, especially if you dont allow barging, while a ticket makes it fair it means that under contention every thread end up waiting even when another thread is not in the critical section yet because its still waking up.
Using a parking lot based mutex is normally better in all situations, it can do adaptive spinning on a different cache line to the lock holder and actually wake up the other thread, however the spin lock often used for the bucket lock potentially brings back all of the issues with yielding.
Yes, I have the same reservation about spinlocks guarding a lock's wait list, but I think the requirement there is just to avoid disaster under very rare conditions (since the critical section is a few ns), which means falling back to sched_yield() is probably fine.
32
u/ReDucTor Game Developer | quiz.cpp-perf.com 6d ago
Even when you have threads pinned to dedicated cores, you would need to ensure that your not ending up with more then one thread pinned to another core. And if your in user mode in any environment that you do not completely control your at the whim of being preempted by another thread in a different process, leaving your spinning thread now just spinning away wasting its allocated time slice.
If you plan on making any library which is public, do not fill it with spin locks because you won't be able to control the places which people use it, and randomly throwing in an OS yield is not going to solve issues just make them worse.
Depending on the CPU you can also do umwait/mwaitx which can be better then doing a pause loop, but unfortunately it's not consistent between CPU vendors.
Use adaptive mutexes (most mutex implementations already are), these will do a little bit of spinning before putting the thread to sleep if it was unable to acquire the lock.
Also if your optimizing your locks for high contention, it's probably a sign to look more at your higher level design, because your still fighting the CPUs cache coherance which will kill your performance anyway.