Lessons learnt while spinning
In a recent project I worked on, I tried to minimize context switches by strategically replacing a critical section in our memory allocator by a custom spinlock that looked like this:
class SpinLock { volatile tInt LockSem; public: FORCEINLINE SpinLock() : LockSem(0) {} FORCEINLINE tBool Lock() { while(1) { // Atomically swap the lock variable with 1 if it's currently equal to 0 if(!InterlockedCompareExchange(&LockSem, 1, 0)) { // We successfully acquired the lock MemoryBarrier(); return; } } } FORCEINLINE void Unlock() { MemoryBarrier(); LockSem = 0; } };
This indeed worked as predicted, but produced an unwanted side-effect on platforms without threads priority inversion (where a high-priority thread spins in a loop while waiting for a low-priority thread to release a lock) as the Xbox 360 (described in this technical article).
It created rare situations where the game was deadlocked, and the time spent to figure-out what was going on was quite high. At first, we played with threads priorities and affinities, but it had other unwanted side-effects. Another solution tried was to yield after a certain amount of spin loops, generating a context switch that let the OS reschedule threads correctly. In the end, we reverted to a critical section.
Lesson learnt? We have to be very careful when busy-waiting, especially on OSs that don’t have priority inversion protection mechanisms. Using the OS synchronization primitives is the best way of coordinating multiple threads. When locking becomes a performance bottleneck, we have to address the underlying problem (using lockless programming, etc).
-
March 8, 2012 at 8:46 amUnderstanding Memory Ordering « 0xjfdube
-
January 3, 2014 at 11:34 amImplementing a recursive read-write spinlock | 0xjfdube
-
January 12, 2014 at 9:25 pmOptimizing the recursive read-write spinlock | 0xjfdube