[Golang (Go)] Use sync.Mutex, sync.RWMutex to lock share data for race condition
sync.Mutex & sync.RWMutex
Mutex and RWMutex are not associated with goroutines, but RWMutex is obviously more suitable for scenarios with more reads and less writes. For read performance only, RWMutex is higher than Mutex, because multiple reads of rwmutex can coexist.
sync.Mutex
A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex.
A Mutex must not be copied after first use.
A Mutex
is a method used as a locking mechanism to ensure that only one Goroutine is accessing the critical section of code at any point of time. This is done to prevent race conditions from happening. sync
package contains the Mutex
. Two methods defined on Mutex
:
-
func (m *Mutex) Lock()
Lock locks m. If the lock is already in use, the calling goroutine blocks until the mutex is available.
-
func (m *Mutex) Unlock()
Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock.
A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it.
Examples
Example 1.
1 | mutex.Lock() |
Example 2.
1 | // main.go |
Run it will output:
1 | go run main.go |
sync.RWMutex
Using RWMutex, a reader/writer mutual exclusion lock which allows any number of readers to hold the lock or one writer. This tends to be more efficient than using a full mutex in situations where you have a high ratio of reads to writes.
A RWMutex must not be copied after first use.
If a goroutine holds a RWMutex for reading and another goroutine might call Lock, no goroutine should expect to be able to acquire a read lock until the initial read lock is released. In particular, this prohibits recursive read locking. This is to ensure that the lock eventually becomes available; a blocked Lock call excludes new readers from acquiring the lock.
-
RWMutex
is based onMutex
. On the basis of Mutex, read and write semaphores are added, and the number of read locks similar to reference counting is used. -
Read locks are compatible with read locks, read locks and write locks are mutually exclusive, and write locks and write locks are mutually exclusive. Only after the lock is released can you continue to apply for mutually exclusive locks:
-
You can apply for multiple read locks at the same time
-
Applying for a write lock when there is a read lock will block, and applying for a read lock when there is a write lock will block
-
As long as there is a write lock, subsequent applications for read locks and write locks will block
There are methods defined on RWMutex
:
-
func (rw *RWMutex) Lock()
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available.
-
func (rw *RWMutex) RLock()
RLock locks rw for reading.
It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. See the documentation on the RWMutex type.
-
func (rw *RWMutex) RUnlock()
RUnlock undoes a single RLock call; it does not affect other simultaneous readers. It is a run-time error if rw is not locked for reading on entry to RUnlock.
-
func (rw *RWMutex) Unlock()
Unlock unlocks rw for writing. It is a run-time error if rw is not locked for writing on entry to Unlock.
As with Mutexes, a locked RWMutex is not associated with a particular goroutine. One goroutine may RLock (Lock) a RWMutex and then arrange for another goroutine to RUnlock (Unlock) it.
Examples
1 | // main.go |
Built-in types suitable for sync.Mutex or sync.RWMutex
To use sync.Mutex and Lock() and Unlock() to protect shared variables of built-in types, because they do not contain Mutex properties. Really reasonable shared variables are those struct types that contain Mutex properties:
1 | type mytype struct { |
At this time, as long as you want to protect the var variable, x.m.Lock()
first, and then x.m.Unlock()
after the var operation is completed. This will ensure that the var field variable in x
must be protected.
FAQs
Lock and Unlock appear unpaired
Lock/Unlock operations do not appear in pairs, and there are two situations:
-
Only with Lock
If there is a goroutine that only performs the Lock operation and not the Unlock operation, then other goroutines will always be blocked (cannot get the lock), and the entire system will eventually crash as more and more blocked goroutines.
-
Only the case of Unlock
If a goroutine only performs the Unlock operation, there will be two situations:
-
If other goroutine hold the lock, the lock will be released.
-
If the lock is in an unoccupied state, it will panic.
-
As mentioned earlier, Mutex does not storage the information of the goroutine, so no matter which goroutine executes the Unlock operation, it will have an unlocking effect.
Copy used Mutex
The synchronization primitives in the sync
package cannot be copied. Mutex
, as the most commonly used synchronization primitive, of course cannot be copied.
The reason is that Mutex
is a stateful object. If you copy a Mutex
, the corresponding state will also be copied. Such a new Mutex
may be in a state of holding locks, waking up, or starving, or even waiting for the number of blocked waits to be far greater than zero.
However, Go has a deadlock check mechanism. If this happens, run it directly, and an error will be reported. In addition, you can also use the go vet tool to actively check for deadlocks.
Example:
1 | // main.go |
Go run directly will report an error, go vet main.go
, the result is as follows:
1 | main.go:9:14: copyF passes lock by value: sync.Mutex |
If you simply copy Mutex
without using it, go run
can run normally, and only go vet
can detect it.
Recursive Locks
After one goroutine preempts the lock, other goroutines cannot preempt the lock, but what if the goroutine locks itself again? Reentrant lock means that the current goroutine can still seize the lock again. Reentrant locks are generally called “recursive locks”
Mutex
is not a reentrant lock! As mentioned earlier, Mutex does not storage the information of the goroutine holding the lock, so it cannot distinguish whether it is reentrant or not.
How to find a way to make Mutex bring the goroutine flag information?
-
Get the goroutine id, and use this id to uniquely identify the goroutine. Some open source packages can do this.
-
The goroutine provides a token by itself, and uses this token to uniquely identify the goroutine.
The ideas of these two ways are to obtain the unique identification of the goroutine, so that when the reentry lock is locked, it can be judged whether the same goroutine is locked.
1 | // RecursiveMutex wraps a Mutex to achieve reentrancy |
In fact, this piece of code only needs to look at the definition of RecursiveMutex in lines 2-6 to guess the general logic: mark the holder of the lock, and use a counter to record the number of reentries. When reentering, the actual operation is counter +1, when unlocking, the counter is -1, until the last time, the lock is really released.
Deadlock
A deadlock is a situation where two or more goroutines are waiting for each other due to contention for resources during the execution process. If there is no external intervention, they will not be able to move forward. This situation is called a deadlock.
Necessary conditions for deadlock:
-
Mutually exclusive: the resources being scrambled are exclusive and not shared
-
Hold and wait: A goroutine holds a resource and is still requesting resources held by other goroutines.
-
Inalienable: the resource can only wait for the goroutine holding it to be released
-
Loop waiting: n goroutines P1-Pn, P1 waits for P2, …, Pn-1 waits for Pn, and finally Pn waits for P1
If you want to avoid deadlock, just destroy one or more of the above 4 conditions.
In fact, the Mutex example used by Copy meets the above four conditions (imagine the main function and copyF as two goroutines).
Especially for the last condition, the main function has to wait for copyF to complete before proceeding to the next step; and copyF is blocked because it cannot get the lock (waiting for main to release the lock), and eventually a deadlock is formed.
References
[1] Mutex | sync - The Go Programming Language - https://golang.org/pkg/sync/#Mutex
[2] sync.Mutex | A Tour of Go - https://tour.golang.org/concurrency/9
[3] Go (Golang) Mutex Tutorial with Examples - golangbot.com - https://golangbot.com/mutex/
[5] RWMutex | sync - The Go Programming Language - https://golang.org/pkg/sync/#RWMutex