This article has participated in the Denver Nuggets Creators Camp 3 “More Productive writing” track, see details: Digg project | creators Camp 3 ongoing, “write” personal impact.

Earlier we introduced Mutex, but today we will introduce RWMutex, or read/write locks. Read/write locking is an improvement over Mutex. In applications where there are many reads and few writes, RWMutex provides a greater concurrency capability than Mutex.

The use of RWMutex can be seen in previous articles on concurrency control, the sync primitive package, and the implementation of read/write locks.

The data structure

SRC /sync/rwmutex.go defines the read/write lock data structure:

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}
Copy the code
  • W: Used to control multiple write locks. To obtain a write lock, the lock must be acquired first. If a write lock is in progress, subsequent write locks will be blocked
  • WriterSem: A semaphore that writes to block waiting and is released when the last reader releases the lock
  • ReaderSem: Semaphore waited by a read-blocking coroutine and released by a coroutine holding a write lock
  • ReaderCount: Records the number of readers
  • ReaderWait: Records the number of readers that are blocked while writing

As you can see, the read/write lock also has a mutex inside it to separate two writes, and several others to separate reads and writes.

The interface definition

RWMutex provides four interfaces to:

  • Rlock() : read lock
  • RUnlock() : releases the read lock
  • Lock() : Write Lock, same as Mutex
  • Unlock() : Unlocks the write, same as Mutex

Lock() implements the logic

Write operations do two things:

  1. Get the mutex
  2. Block waiting for all reads to finish (if any)

Func (rw *RWMutex) Lock() interface

Unlock() implements logic

Unwrite does two things:

  1. Wake up coroutines blocked by read locks (if any)
  2. Remove the mutex

Func (rw *RWMutex) Unlock() interface

RLock() implements the logic

Read locking does two things:

  1. Increase the read count, readerCount++
  2. Block waiting for write operations to end (if any)

Func (rw *RWMutex) RLock() interface implementation process

RUnlock() implements the logic

Unlocking a read lock does two things:

  1. Reduce the read operation count, readerCount –
  2. Wake up the write waiting coroutine (if any)

Func (rw *RWMutex) RUnlock() interface

Note: Only the last coroutine to release a read lock will release a semaphore, waking up coroutines that block waiting for a write operation.

Usage scenarios

Write operation prevents write operation

A read/write lock contains a Mutex that must be acquired by a write operation. If the Mutex has been acquired by another coroutine, the current coroutine blocks and waits for the Mutex.

Write operation prevents read operation

Rwmutex. readerCount is an integer indicating the number of readers. If write operations are not taken into account, the value is +1 for each read lock and -1 for each read unlock. It actually supports a maximum of 2^30 concurrent readers.

When the write lock is in progress, the readerCount is subtracted by 2^30, and the readerCount becomes a negative value. When the read lock is in progress, the readerCount is detected to be a negative value. The actual number of reads is not lost, just add readerCount to 2^30.

So, writes turn readerCount negative to prevent reads.

Read operation prevents write operation

ReaderCount is added to the rwmutext. readerCount by 1. In this case, when the number of readers is not 0 when the write operation arrives, the rwmutext. readerCount is blocked until all the read operations are complete.

Therefore, the read operation blocks the write operation in the future by readerCount.

Why won’t write locks starve to death

A write operation can acquire the lock only after the read operation finishes. New reads may continue to arrive during the write operation. If a write operation waits for all reads to finish, it is likely to starve to death.

However, rwmutex.readerwait solves this problem perfectly. When a write operation arrives, the rwmutex. readerCount value is copied to the rwmutex. readerWait, which is used to mark the number of readers before the write operation. The rwmutex. readerWait value is decremented and the rwmutex. readerWait value is 0.

So, as shown in the figure above, the previous read operation wakes up the write operation, and the subsequent read operation wakes up the write operation.