If you haven’t read the mutex article, please read my article about mutex first. RWmutex is implemented based on mutex, just like dancing on the shoulders of giants.
https://medium.com/gitconnected/go-source-code-sync-mutex-3082a25ef092
This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
In the process of using mutex, whether it is reading or writing, we use Mutex to ensure that only one goroutine accesses the shared resource. This is a bit “wasteful” in some cases. For example, in the case of few writes and many reads, even if there is no write operation for a period of time, a large number of concurrent read accesses have to be serialized under the protection of Mutex. At this time, using Mutex has a significant impact on performance. RWmutex is designed to optimize this problem.
What is RWMutex
Let’s explain the read-write lock RWMutex. The RWMutex in the standard library is a reader/writer mutex. At any given time, it can be held by any number of readers or by a single writer. This is the readers-writers problem, which can generally be divided into three categories based on the priority of read and write operations. The design and implementation of read-write locks are also divided into three categories based on the priority.
- Read-preferring: A read-preferring design can provide high concurrency, but in highly competitive situations, it may lead to write starvation. This is because, if there are a large number of reads, this design will cause the write to only be able to acquire the lock after all the reads have released the lock.
- Write-preferring: A write-preferring design means that if there is already a writer waiting for the lock, it will prevent new reader requests from acquiring the lock, thus prioritizing the writer. Of course, if some readers have already requested the lock, the new writer will also wait until the existing readers have released the lock before it can acquire it. Therefore, the priority in the write-preferring design is for new requests. This design mainly avoids writer starvation problems.
- No specified priority: This design is relatively simple and does not distinguish between reader and writer priorities. In some scenarios, this design without specified priorities is more effective. The first type of priority can lead to write starvation, and the second type of priority may lead to read starvation. This access without specified priorities does not distinguish between reading and writing, and everyone has the same priority, solving the starvation problem.
The RWMutex design in the Go standard library is a write-preferring solution. A blocking Lock call excludes new reader requests from acquiring the lock.
Implementation of RWMutex
This article is based on go 1.21.4
|
|
- The field readerCount records the current number of readers (and whether there is a writer competing for the lock).
- readerWait records the number of readers that need to wait for read completion when a writer requests the lock.
Implementation of RLock/RUnlock
|
|
The second line adds 1 to the reader count. The readerCount field has a dual meaning: when there is no writer competing for or holding the lock, the readerCount is the same as the normal reader count we understand; if there is a writer competing for or holding the lock, the readerCount not only serves as the reader count, but also indicates whether there is a writer competing for or holding the lock (as explained later). In this case, the reader is blocked and waits for the writer to wake up (the fourth line).
When calling RUnlock, we need to subtract 1 from the reader count (the eighth line) because the number of readers has decreased by one. However, the return value of AddInt32 in the eighth line has another meaning. If it is negative, it means that there is a writer competing for the lock. In this case, the rUnlockSlow method is called to check whether all readers have released the read lock. If all readers have released the read lock, the waiting writer can be awakened. When one or more readers hold the lock, the writer competing for the lock waits for these readers to release the lock before it can hold the lock.
LOCK
|
|
RWMutex is a multiple writer and multiple reader read-write lock, so there may be multiple writers and readers at the same time. In order to avoid writer competition, RWMutex uses a Mutex to ensure writer mutual exclusion. Once a writer obtains the internal mutex, it reverses the readerCount field from the original positive integer readerCount (>=0) to a negative number (readerCount-rwmutexMaxReaders), so that this field has two meanings (both the number of readers and whether there is a writer). Let’s look at the following code. In the fifth line, it also records the current number of active readers. The so-called active readers are those readers who hold the read lock and have not released it yet.
If readerCount is not 0, it means that there are readers holding the read lock, and RWMutex needs to save the current readerCount to the readerWait field (seventh line). At the same time, this writer enters a blocking waiting state (eighth line). Each time a reader releases the read lock (when calling the RUnlock method), the readerWait field is reduced by 1 until all active readers release the read lock, and then this writer can be awakened.
Unlock
|
|
When a writer releases the lock, it reverses the readerCount field again (from a negative number to a positive number), and then wakes up the blocked readers (the fourth to sixth lines). Before returning from Unlock in RWMutex, the internal mutex needs to be released. After releasing it, other writers can continue to compete for the lock.
TryLock & TryRLock
The implementations of TryRLock
and TryLock
are both simple. They attempt to acquire the read lock or write lock, and if they fail, they return false. If they succeed, they return true. These two methods do not block and wait. Let’s look at the source code directly.
|
|
Controversial Points
The controversial points of Mutex also exist in RWMutex, such as non-reentrancy and non-copyability. When writing code, you need to consider them carefully.
|
|
Summary
- RWMutex is implemented based on Mutex.
- In scenarios with many reads and few writes, we can use RWMutex instead of Mutex to improve performance.
- At the same time, multiple readers can hold RWMutex through RLock().
- When a writer locks RWMutex through Lock(), it first prevents new readers from locking, and then waits for all readers to release the lock before locking.
- When a writer releases the lock through ULock(), it wakes up the waiting readers.