[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
2
3
4
5
mutex.Lock() 

x = x + 1 // this statement be executed by only one Goroutine at any point of time

mutex.Unlock()

Example 2.

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
30
31
32
33
34
35
36
37
38
39
40
41
// main.go

package main

import (
"fmt"
"sync"
"time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}

Run it will output:

1
2
$ go run main.go
1000

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 on Mutex. 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
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// main.go

package main

import (
"sync"
)

type RWMap struct {
sync.RWMutex
m map[string]int
}

// Get is a wrapper for getting the value from the underlying map
func (r RWMap) Get(key string) int {
r.RLock()
defer r.RUnlock()
return r.m[key]
}

// Set is a wrapper for setting the value of a key in the underlying map
func (r RWMap) Set(key string, val int) {
r.Lock()
defer r.Unlock()
r.m[key] = val
}

// Inc increases the value in the RWMap for a key.
// This is more pleasant than r.Set(key, r.Get(key)++)
func (r RWMap) Inc(key string) {
r.Lock()
defer r.Unlock()
r.m[key]++
}

func main() {

// Init
counter := RWMap{m: make(map[string]int)}

// Get a Read Lock
counter.RLock()
_ = counter.m["Key"]
counter.RUnlock()

// the above could be replaced with
_ = counter.Get("Key")

// Get a write Lock
counter.Lock()
counter.m["some_key"]++
counter.Unlock()

// above would need to be written as
counter.Inc("some_key")
}

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
2
3
4
5
6
type mytype struct {
m sync.Mutex // Or sync.RWMutex
var int
}

x := new(mytype)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.go

package main

import "sync"

func copyF(m sync.Mutex) {
m.Lock()
defer m.Unlock()
// TODO
return
}

func main() {
var m sync.Mutex
m.Lock()
defer m.Unlock()
// TODO
copyF(m) // m is passed in by copying
return
}

Go run directly will report an error, go vet main.go, the result is as follows:

1
2
main.go:9:14: copyF passes lock by value: sync.Mutex
main.go:24:8: call of copyF copies lock 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
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
30
31
32
33
34
35
// RecursiveMutex wraps a Mutex to achieve reentrancy
type RecursiveMutex struct {
sync.Mutex
owner int64 // The goroutine id currently holding the lock
recursion int32 // The number of times this goroutine reenters
}

func (m *RecursiveMutex) Lock() {
gid := goid.Get()
// If the goroutine currently holding the lock is the goroutine called this time, it means reentrant
if atomic.LoadInt64(&m.owner) == gid {
m.recursion++
return
}
m.Mutex.Lock()
// The first call of the goroutine that acquired the lock, record its goroutine id, and add 1 to the number of calls
atomic.StoreInt64(&m.owner, gid)
m.recursion = 1
}

func (m *RecursiveMutex) Unlock() {
gid := goid.Get()
// The goroutine that does not hold the lock tries to release the lock and uses it incorrectly
if atomic.LoadInt64(&m.owner) != gid {
panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
}
// The number of calls minus 1
m.recursion--
if m.recursion != 0 {// If this goroutine has not been completely released, return directly
return
}
// The last time this goroutine is called, the lock needs to be released
atomic.StoreInt64(&m.owner, -1)
m.Mutex.Unlock()
}

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/

[4] Using Synchronization Primitives in Go | by Abhishek Gupta | Better Programming - https://betterprogramming.pub/using-synchronization-primitives-in-go-mutex-waitgroup-once-2e50359cb0a7

[5] RWMutex | sync - The Go Programming Language - https://golang.org/pkg/sync/#RWMutex

[6] Golang Mutexes – What Is RWMutex For? - DEV Community - https://dev.to/wagslane/golang-mutexes-what-is-rwmutex-for-57a0

[7] Go - Concurrent Access of Maps | go Tutorial - https://riptutorial.com/go/example/3423/concurrent-access-of-maps

[8] Recursive Locking in Go. Go’s built-in Mutex doesn’t support… | by bytecraze | Medium - https://medium.com/@bytecraze.com/recursive-locking-in-go-9c1c2a106a38