sync: implement RWMutex using futexes

Somewhat surprisingly, this results in smaller code than the old code
with the cooperative (tasks) scheduler. Probably because the new RWMutex
is also simpler.
This commit is contained in:
Ayke van Laethem
2024-11-02 15:33:06 +01:00
parent 3cc9c44c26
commit 8d6e16019a
+62 -97
View File
@@ -6,131 +6,96 @@ import (
type Mutex = task.Mutex type Mutex = task.Mutex
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(msg string)
type RWMutex struct { type RWMutex struct {
// waitingWriters are all of the tasks waiting for write locks. // Reader count, with the number of readers that currently have read-locked
waitingWriters task.Stack // this mutex.
// The value can be in two states: one where 0 means no readers and another
// where -rwMutexMaxReaders means no readers. A base of 0 is normal
// uncontended operation, a base of -rwMutexMaxReaders means a writer has
// the lock or is trying to get the lock. In the second case, readers should
// wait until the reader count becomes non-negative again to give the writer
// a chance to obtain the lock.
readers task.Futex
// waitingReaders are all of the tasks waiting for a read lock. // Writer futex, normally 0. If there is a writer waiting until all readers
waitingReaders task.Stack // have unlocked, this value is 1. It will be changed to a 2 (and get a
// wake) when the last reader unlocks.
writer task.Futex
// state is the current state of the RWMutex. // Writer lock. Held between Lock() and Unlock().
// Iff the mutex is completely unlocked, it contains rwMutexStateUnlocked (aka 0). writerLock Mutex
// Iff the mutex is write-locked, it contains rwMutexStateWLocked.
// While the mutex is read-locked, it contains the current number of readers.
state uint32
} }
const ( const rwMutexMaxReaders = 1 << 30
rwMutexStateUnlocked = uint32(0)
rwMutexStateWLocked = ^uint32(0)
rwMutexMaxReaders = rwMutexStateWLocked - 1
)
func (rw *RWMutex) Lock() { func (rw *RWMutex) Lock() {
if rw.state == 0 { // Exclusive lock for writers.
// The mutex is completely unlocked. rw.writerLock.Lock()
// Lock without waiting.
rw.state = rwMutexStateWLocked // Flag that we need to be awakened after the last read-lock unlocks.
rw.writer.Store(1)
// Signal to readers that they can't lock this mutex anymore.
n := uint32(rwMutexMaxReaders)
waiting := rw.readers.Add(-n)
if int32(waiting) == -rwMutexMaxReaders {
// All readers were already unlocked, so we don't need to wait for them.
rw.writer.Store(0)
return return
} }
// Wait for the lock to be released. // There is at least one reader.
rw.waitingWriters.Push(task.Current()) // Wait until all readers are unlocked. The last reader to unlock will set
task.Pause() // rw.writer to 2 and awaken us.
for rw.writer.Load() == 1 {
rw.writer.Wait(1)
}
rw.writer.Store(0)
} }
func (rw *RWMutex) Unlock() { func (rw *RWMutex) Unlock() {
switch rw.state { // Signal that new readers can lock this mutex.
case rwMutexStateWLocked: waiting := rw.readers.Add(rwMutexMaxReaders)
// This is correct. if waiting != 0 {
// Awaken all waiting readers.
case rwMutexStateUnlocked: rw.readers.WakeAll()
// The mutex is already unlocked.
panic("sync: unlock of unlocked RWMutex")
default:
// The mutex is read-locked instead of write-locked.
panic("sync: write-unlock of read-locked RWMutex")
} }
switch { // Done with this lock (next writer can try to get a lock).
case rw.maybeUnblockReaders(): rw.writerLock.Unlock()
// Switched over to read mode.
case rw.maybeUnblockWriter():
// Transferred to another writer.
default:
// Nothing is waiting for the lock.
rw.state = rwMutexStateUnlocked
}
} }
func (rw *RWMutex) RLock() { func (rw *RWMutex) RLock() {
if rw.state == rwMutexStateWLocked { // Add us as a reader.
// Wait for the write lock to be released. newVal := rw.readers.Add(1)
rw.waitingReaders.Push(task.Current())
task.Pause()
return
}
if rw.state == rwMutexMaxReaders { // Wait until the RWMutex is available for readers.
panic("sync: too many readers on RWMutex") for int32(newVal) <= 0 {
rw.readers.Wait(newVal)
newVal = rw.readers.Load()
} }
// Increase the reader count.
rw.state++
} }
func (rw *RWMutex) RUnlock() { func (rw *RWMutex) RUnlock() {
switch rw.state { // Remove us as a reader.
case rwMutexStateUnlocked: one := uint32(1)
// The mutex is already unlocked. readers := int32(rw.readers.Add(-one))
panic("sync: unlock of unlocked RWMutex")
case rwMutexStateWLocked: // Check whether RUnlock was called too often.
// The mutex is write-locked instead of read-locked. if readers == -1 || readers == (-rwMutexMaxReaders)-1 {
panic("sync: read-unlock of write-locked RWMutex") runtimePanic("sync: RUnlock of unlocked RWMutex")
} }
rw.state-- if readers == -rwMutexMaxReaders {
// This was the last read lock. Check whether we need to wake up a write
if rw.state == rwMutexStateUnlocked { // lock.
// This was the last reader. if rw.writer.CompareAndSwap(1, 2) {
// Try to unblock a writer. rw.writer.Wake()
rw.maybeUnblockWriter()
}
}
func (rw *RWMutex) maybeUnblockReaders() bool {
var n uint32
for {
t := rw.waitingReaders.Pop()
if t == nil {
break
} }
n++
scheduleTask(t)
} }
if n == 0 {
return false
}
rw.state = n
return true
}
func (rw *RWMutex) maybeUnblockWriter() bool {
t := rw.waitingWriters.Pop()
if t == nil {
return false
}
rw.state = rwMutexStateWLocked
scheduleTask(t)
return true
} }
type Locker interface { type Locker interface {