|
| 1 | +//go:build scheduler.threads |
| 2 | + |
| 3 | +package task |
| 4 | + |
| 5 | +// Futex-based mutex. |
| 6 | +// This is largely based on the paper "Futexes are Tricky" by Ulrich Drepper. |
| 7 | +// It describes a few ways to implement mutexes using a futex, and how some |
| 8 | +// seemingly-obvious implementations don't exactly work as intended. |
| 9 | +// Unfortunately, Go atomic operations work slightly differently so we can't |
| 10 | +// copy the algorithm verbatim. |
| 11 | +// |
| 12 | +// The implementation works like this. The futex can have 3 different values, |
| 13 | +// depending on the state: |
| 14 | +// |
| 15 | +// - 0: the futex is currently unlocked. |
| 16 | +// - 1: the futex is locked, but is uncontended. There is one special case: if |
| 17 | +// a contended futex is unlocked, it is set to 0. It is possible for another |
| 18 | +// thread to lock the futex before the next waiter is woken. But because a |
| 19 | +// waiter will be woken (if there is one), it will always change to 2 |
| 20 | +// regardless. So this is not a problem. |
| 21 | +// - 2: the futex is locked, and is contended. At least one thread is trying |
| 22 | +// to obtain the lock (and is in the contended loop, see below). |
| 23 | +// |
| 24 | +// For the paper, see: |
| 25 | +// https://dept-info.labri.fr/~denis/Enseignement/2008-IR/Articles/01-futex.pdf) |
| 26 | + |
| 27 | +type Mutex struct { |
| 28 | + futex Futex |
| 29 | +} |
| 30 | + |
| 31 | +func (m *Mutex) Lock() { |
| 32 | + // Fast path: try to take an uncontended lock. |
| 33 | + if m.futex.CompareAndSwap(0, 1) { |
| 34 | + // We obtained the mutex. |
| 35 | + return |
| 36 | + } |
| 37 | + |
| 38 | + // The futex is contended, so we enter the contended loop. |
| 39 | + // If we manage to change the futex from 0 to 2, we managed to take the |
| 40 | + // lock. Else, we have to wait until a call to Unlock unlocks this mutex. |
| 41 | + // (Unlock will wake one waiter when it finds the futex is set to 2 when |
| 42 | + // unlocking). |
| 43 | + for m.futex.Swap(2) != 0 { |
| 44 | + // Wait until we get resumed in Unlock. |
| 45 | + m.futex.Wait(2) |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +func (m *Mutex) Unlock() { |
| 50 | + if old := m.futex.Swap(0); old == 0 { |
| 51 | + // Mutex wasn't locked before. |
| 52 | + panic("sync: unlock of unlocked Mutex") |
| 53 | + } else if old == 2 { |
| 54 | + // Mutex was a contended lock, so we need to wake the next waiter. |
| 55 | + m.futex.Wake() |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +// TryLock tries to lock m and reports whether it succeeded. |
| 60 | +// |
| 61 | +// Note that while correct uses of TryLock do exist, they are rare, |
| 62 | +// and use of TryLock is often a sign of a deeper problem |
| 63 | +// in a particular use of mutexes. |
| 64 | +func (m *Mutex) TryLock() bool { |
| 65 | + // Fast path: try to take an uncontended lock. |
| 66 | + if m.futex.CompareAndSwap(0, 1) { |
| 67 | + // We obtained the mutex. |
| 68 | + return true |
| 69 | + } |
| 70 | + return false |
| 71 | +} |
0 commit comments