Another Spinlock Question
This question is adapted from my year’s Advanced Operating Systems finals. I didn’t check the answer scheme / results for this, but I’m fairly confident this is somewhat correct. I’m writing this to scratch that concurrency itch.
Consider this atomic instruction: lock-xadd()
, where it has the side effects of the following C code:
|
|
Question: Implement spin_lock()
and spin_unlock()
in C using the lock_xadd()
function.
Implementation
The insight: we can get the ‘old’ state of the atomic operation, since *src = *dst
. This means that we can implement this spinlock with a compare-and-swap pattern.
|
|
Explanation
Remember that atomic instructions are atomic. We are guaranteed that each thread will indivisibly operate on lock.
For spin_lock, we add 1 to the lock to try and acquire it. Since operations are atomic, the returned value add
tells us the previous state of lock
.
If add==0
, we know for sure the operation incremented lock from 0 -> 1. This means that we acquired the lock successfully, as we are the only lock acquirer that accessed the state of 0 for lock.
Else, the operation worked on a positive nonzero value of lock (meaning it was locked by someone else already), so we spin.
The spin operation is incredibly fun when I thought of the solution. Typically we would use some powerful compare exchange atomic instruction to do compare-and-swap. We don’t have the ability to do a CMP and SET atomically in this problem. The idea is to just separate both operations.
We can exploit the idempotency of 0 w.r.t. the addition operation. We can exploit this to atomically check the state of lock
when we do tmp = 0; lock_xadd(&tmp, &lock);
.
Only an idempotent operation here will work. Why?
If we increment, then our lock will starve or overflow. (acquire keeps adding, unlock keeps decrementing). We cannot decrement as it will conflict with the unlock operation.
Once we are sure that the lock state is 0, then we try to acquire. This should keep all additions to lock
finitely bounded.
For spin_unlock, we just keep decrementing until lock
is 0 (unlocked) 👍
Limitations & Discussion
We follow the pthread model, where spin_unlock
can only be unlocked by the lock holding thread. See this docs on pthread_spin_unlock
. If this invariant does not hold, then exclusive access to the lock (for decrement) is violated. We get the funky lock < 0 situation.
If there are an unbounded number of lock acquirers, it could be theoretically possible to starve all acquirers since the unlocking thread can only decrement one at a time. One does not simply set lock
to 0 with lock_xadd()
. This shouldn’t be possible (left as an exercise).
Practically, unbounded acquires should not happen. Progress will eventually be made, since for a finite number of acquires N
, it will just take O(N
)1 spins by the unlocker to eventually decrement lock
to 0. No starvation!
You also have the problem of data representation where we have more acquires than the int64_t
can fit in the positive range. Then we get an overflow. But this is an unsolveable problem given the constraints, since we can either kick the can down the road by using uint64_t
or a larger int representation (whatever this may be).
Lastly, note that the implementation I provided is not as fast as the real spinlock/unlock due to the limitations of the atomic instruction we are given to implement.
-
In the actual semantics of big O, where I mean the worst case. ↩︎