Mutex is used to ensure that only one goroutine accesses a shared resource. In a large number of concurrent scenarios, especially read scenarios, a shared resource block can only be accessed serially by a goroutine, which leads to performance impact, and the solution is to distinguish between read and write operations.

This turns a serial read into a parallel read, which is used to improve the performance of read operations.

Go standard library RWMutex (read/write lock) is used to solve the readers-writes problem.

RWMutex

The RWMutex in the standard library is a reader/writer mutex, RWMutex can only be held by n readers at a time, or by a single writer.

  • Lock/Unlock: method called on write operation, if held by reader or writer, Lock blocks until lock can be acquired, Unlock is to release lock.
  • Rlock/RUnlock: method to be called for read operation, if it is already held by writer, Rlock will keep blocking until the lock is obtained, otherwise it will return directly, RUlock is the method to release the lock by reader.
  • RLocker: return a Locker interface object for the read operation.

The zero value of RWMutex is the unlocked state, so there is no need for explicit initialization when using RWMutex as a variable or embedding it in a struct.

Implementation Principles

For readers-writers problem is based on the priority of read and write operations, the design of read-write locks is divided into three categories.

  • Read-preferring read-priority design : good for concurrency, but can lead to write starvation in a large number of concurrent scenarios.
  • Write-preferring write-preferring design : for new requests, mainly to avoid the writer starvation problem, that is, there is a reader and writer waiting to get the lock at the same time, will give priority to the writer.
  • No priority specified: FIFO, does not distinguish between read and write priorities, and is suitable for some specific scenarios.

RWMutex design is write-preferring write-first design. A Lock call that is blocking will expel a new reader request to the lock.

RWMutex contains a Mutex, and four auxiliary fields writerSem, readerSem, readerCount, and readerWait.

1
2
3
4
5
6
7
8
9
type RWMutex struct {
  w           Mutex   // mutually exclusive locks to resolve competition between multiple writers
  writerSem   uint32  // writer
  readerSem   uint32  // reader
  readerCount int32   // The number of readers, recording the number of readers currently waiting
  readerWait  int32   // writer Number of readers waiting to be completed
}

const rwmutexMaxReaders = 1 << 30

Implementation of RLock/RUlock

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (rw *RWMutex) RLock() {
    // For reader count +1, readerCount will have a negative number
    // 1. When there is no writer competing or holding a lock, readerCount acts as a counter
    // 2. If there is a writer competing for a lock or holding a lock, then readerCount not only serves as a counter for the reader, but also identifies whether there is a writer competing for or holding a lock
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // When rw.readerCount is negative, it means that there is a writer waiting for a lock request
        // Because the writer has high priority, the later reader is blocked to sleep
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

func (rw *RWMutex) RUnlock() {
    // count the reader as -1
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        // If it is negative, it means that there is a writer competing for the lock, check if all readers have released the lock
        // If they have, let the writer get the lock and write
        rw.rUnlockSlow(r) // There are waiting writers
    }
}

func (rw *RWMutex) rUnlockSlow(r int32) {
    // rUnlockSlow counts the readers holding the lock as -1.
    // will check if all existing readers have released their locks.
    // If they have all released the lock, it will wake up the writer and let the writer hold the lock.
    if atomic.AddInt32(&rw.readerWait, -1) == 0 {
        // The last reader, the writer finally has a chance to get a lock
        runtime_Semrelease(&rw.writerSem, false, 1)
    }
}

Lock / Unlock

RWMutex is a multi-writer multi-reader lock, so there may be more than one writer and reader at the same time. then, to avoid competition between writers, RWMutex uses a Mutex to ensure mutual exclusion of writers. The RWMutex uses a Mutex to ensure mutual exclusion of writers to avoid competition between writers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (rw *RWMutex) Lock() {
    // Solve other writer competition problems first
    rw.w.Lock()
    // Invert readerCount to tell the reader that the writer has competing locks
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    // If there is currently a reader holding the lock, then you need to wait for
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}

Once a writer has acquired an internal mutex lock, the readerCount field is inverted, changing it from a positive integer readerCount(>=0) to a negative number (readerCount-rwmutexMaxReaders) so that the field maintains two meanings (it holds the number of readers and indicates that there is currently a writer).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func (rw *RWMutex) Unlock() {
    // Tell the reader there are no more active writers
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    
    // Wake up blocked readers
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    // Release internal mutual exclusion locks
    rw.w.Unlock()
}

When a writer releases a lock, it reverses the readerCount field again. To be sure, since the current lock is held by the writer, the readerCount field is inverted and the constant rwmutexMaxReaders is subtracted to make it a negative number.

So, the inverse method here is to add the constant rwmutexMaxReaders to it. Now that the writer is releasing the lock, it’s time to wake up the new readers and let them continue their execution happily without blocking them.

Before the RWMutex’s Unlock returns, the internal mutex lock needs to be released. After it is released, other writers can continue to compete for the lock.

Three common mistakes made by RWMutex

  • Non-replication
  • Deadlock due to reentry
  • Releasing an unlocked RWMutex