In the Go language, the sync package provides mutex Mutex and read-write lock RWMutex are used to handle the situation where two or more coroutines (or threads) may read or write the same variable at the same time during concurrency.
Why need a lock
Lock is the core of the sync package. It has two main methods, namely Lock and Unlock.
In concurrency, multiple threads or coroutines modify a variable at the same time. Using locks can ensure that only one coroutine or thread modify this variable at a certain time.
When locks are not used, the desired result may not be obtained in concurrency, as shown below:
package main import ( "fmt" "time" ) func main() { var a = 0 for i := 0; i < 1000; i++ { go func(idx int) { a += 1 (a) }(i) } () }
Theoretically, the above program will increment the value of a sequentially, but the actual result is as follows.
537
995
996
997
538
999
1000
Through the running results, we can see that the value of a is not output in increments in order. Why is this?
The execution order of coroutines is roughly as follows:
- Read the value of a from the register;
- Then do the addition operation;
- Finally write to the register.
In the above order, if a coroutine obtains the value of a is 3 and then performs the addition operation, then another coroutine takes the value of a, and the value obtained is also 3, and the final result of the two coroutines is the same.
The concept of locking is that when a coroutine is processing a, a is locked, and other coroutines need to wait for the coroutine to complete processing and unlock a before operating, that is, there can only be one coroutine that processes a at the same time, thereby avoiding the situation in the above example.
Mutex
How to solve the problems that arise in the above example? Just add a mutex to Mutex. So what is a mutex? There are two methods in a mutex that can be called, as shown below:
func (m *Mutex) Lock() func (m *Mutex) Unlock()
Change the above code slightly, as follows:
package main import ( "fmt" "sync" "time" ) func main() { var a = 0 var lock for i := 0; i < 1000; i++ { go func(idx int) { () defer () a += 1 ("goroutine %d, a=%d\n", idx, a) }(i) } // Wait for 1s to end the main program // Make sure all coroutines are executed () }
The operation results are as follows:
goroutine 995, a=996
goroutine 996, a=997
goroutine 997, a=998
goroutine 998, a=999
goroutine 999, a=1000
It should be noted that a mutex can only be locked by one goroutine at the same time, and other goroutines will block until the mutex is unlocked (re-compete for the lock of the mutex). The example code is as follows:
package main import ( "fmt" "sync" "time" ) func main() { ch := make(chan struct{}, 2) var l go func() { () defer () ("goroutine1: I will lock for about 2s") ( * 2) ("goroutine1: I unlocked it, you guys go grab it") ch <- struct{}{} }() go func() { ("goroutine2: Waiting for unlocking") () defer () ("goroutine2: Oye, I unlocked it too") ch <- struct{}{} }() // Wait for the end of goroutine execution for i := 0; i < 2; i++ { <-ch } }
The above code runs as follows:
goroutine1: I will lock for about 2s
goroutine2: Waiting for unlocking
goroutine1: I unlocked it, you guys go grab it
goroutine2: Oye, I unlocked it too
Read and write lock
There are four methods for reading and writing locks:
- The locking and unlocking of write operations are func (*RWMutex) Lock and func (*RWMutex) Unlock, respectively;
- The locking and unlocking of read operations are func (*RWMutex) Rlock and func (*RWMutex) RUnlock, respectively.
The difference between read and write locks is:
- When a goroutine obtains a write lock, the rest, whether it is a read lock or a write lock, will be blocked until the write unlocks;
- When a goroutine obtains a read lock, other read locks can still continue;
- When there are one or any number of read locks, the write lock will wait until all read locks are unlocked before the write lock can be performed.
Therefore, the purpose of read lock (RLock) here is actually to tell write lock. There are many coroutines or processes that are reading data. Write operations need to wait until they are read (read unlocked) before they can be written (write lock).
We can summarize it into three items:
- At the same time, only one goroutine can obtain write locks;
- At the same time, you can have as many gorouintes to obtain read locks;
- At the same time, only write locks or read locks (read and write mutexes).
The sample code is as follows:
package main import ( "fmt" "math/rand" "sync" ) var count int var rw func main() { ch := make(chan struct{}, 10) for i := 0; i < 5; i++ { go read(i, ch) } for i := 0; i < 5; i++ { go write(i, ch) } for i := 0; i < 10; i++ { <-ch } } func read(n int, ch chan struct{}) { () ("goroutine %d enters read operation...\n", n) v := count ("goroutine %d read ends, value is: %d\n", n, v) () ch <- struct{}{} } func write(n int, ch chan struct{}) { () ("goroutine %d enters write operation...\n", n) v := (1000) count = v ("goroutine %d write ends, the new value is: %d\n", n, v) () ch <- struct{}{} }
The execution results are as follows:
goroutine 0 Enter read operation...
goroutine 0 The reading ends, the value is: 0
goroutine 3 Enter the read operation...
goroutine 1 Enter the read operation...
goroutine 3 The reading ends, the value is: 0
goroutine 1 The reading ends, the value is: 0
goroutine 4 Enter the write operation...
goroutine 4 The write ends, the new value is: 81
goroutine 4 Enter the read operation...
goroutine 4 The reading ends, the value is: 81
goroutine 2 Enter the read operation...
goroutine 2 The reading ends, the value is: 81
goroutine 0 Enter write operation...
goroutine 0 The write ends, the new value is: 887
goroutine 1 Enter write operation...
goroutine 1 The write ends, the new value is: 847
goroutine 2 Enter write operation...
goroutine 2 The write ends, the new value is: 59
goroutine 3 Enter write operation...
goroutine 3 write ends, new value is: 81
Let’s take a look at two more examples.
[Example 1] When multiple read operations read a variable at the same time, although locks are added, the read operations are not affected. (Read and write are mutually exclusive, read and read are not mutually exclusive)
package main import ( "sync" "time" ) var m * func main() { m = new() // Read multiple simultaneously go read(1) go read(2) (2*) } func read(i int) { println(i,"read start") () println(i,"reading") (1*) () println(i,"read over") }
The operation results are as follows:
1 read start
1 reading
2 read start
2 reading
1 read over
2 read over
[Example 2] Because read and write are mutually exclusive, when the write operation starts, the read operation must wait until the write operation is completed before continuing, otherwise the read operation can only continue to wait.
package main import ( "sync" "time" ) var m * func main() { m = new() // Can't do anything when writing go write(1) go read(2) go write(3) (2*) } func read(i int) { println(i,"read start") () println(i,"reading") (1*) () println(i,"read over") } func write(i int) { println(i,"write start") () println(i,"writing") (1*) () println(i,"write over") }
The operation results are as follows:
1 write start
3 write start
1 writing
2 read start
1 write over
2 reading
This is the article about Go sync package and lock implementation restricting thread access to variables. For more related Go sync package and lock content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!