In concurrent programming, multipleGoroutine
Race conditions may occur when accessing the same piece of memory resource, and we need to use appropriate synchronization operations in the critical section to avoid race conditions. There are many synchronization tools provided in the Go language, and this article will introduce mutex locks.Mut
ex and read and write locksRWMutex
How to use it.
1. Mutex
1. Introduction to Mutex
The synchronization tool for Go language is mainly composed ofsync
Package provided, mutex (Mutex
) and read and write lock (RWMutex
) is the method in the sync package.
Mutex can be used to protect a critical area to ensure that there is only one at the same time.goroutine
In this critical area. It mainly includes two operations: lock (Lock method) and unlock (Unlock method). First, it is used to enter the critical area.goroutine
Lock and unlock when leaving.
The following points should be paid attention to when using a mutex:
- Do not repeatedly lock the mutex, otherwise it will block and may also cause deadlock (
deadlock
); - It is also necessary to unlock the mutex to avoid repeated locking;
- Do not unlock unlocked or unlocked mutexes;
- Do not pass mutexes directly between multiple functions.
The type belongs to the value type. When passing it to a function, a copy will be generated. The operation of the lock in the function will not affect the original lock.
In short, a mutex is only used to protect one critical area. Remember to unlock it after locking. For each lock operation, there must be only one corresponding unlock operation, that is, locking and unlocking must appear in pairs, and the safest way is used.defer
Statement Unlock.
2. Mutex usage example
The following code simulates withdrawal and save money:
package main import ( "flag" "fmt" "sync" ) var ( mutex balance int protecting uint // Whether to add lock sign = make(chan struct{}, 10) //Channel, used to wait for all goroutines) // Save moneyfunc deposit(value int) { defer func() { sign <- struct{}{} }() if protecting == 1 { () defer () } ("Balance: %d\n", balance) balance += value ("Balance after saving %d: %d\n", value, balance) () } // Withdraw moneyfunc withdraw(value int) { defer func() { sign <- struct{}{} }() if protecting == 1 { () defer () } ("Balance: %d\n", balance) balance -= value ("Balance after %d: %d\n", value, balance) () } func main() { for i:=0; i < 5; i++ { go withdraw(500) // Get 500 go deposit(500) // Save 500 } for i := 0; i < 10; i++ { <-sign } ("Current balance: %d\n", balance) } func init() { balance = 1000 // The initial account balance is 1000 (&protecting, "protecting", 0, "Whether to lock it, 0 means no locking, 1 means locking") }
In the above code, a channel is used to make the maingoroutine
Wait for othergoroutine
Running ends, each childgoroutine
Send an element to the channel before the run ends,goroutine
At the end, the element is received from this channel, the number of times is received and the subgoroutine
The number is the same. After receiving it, it will exit the maingoroutine
。
The code uses coroutines to implement the operation of saving and withdrawing money on an account multiple times (5 times).Let’s first look at the situation where no lock is added:
Balance: 1000 live 500 后的Balance: 1500 Balance: 1000 Pick 500 后的Balance: 1000 Balance: 1000 live 500 后的Balance: 1500 Balance: 1000 Pick 500 后的Balance: 1000 Balance: 1000 live 500 后的Balance: 1500 Balance: 1000 Pick 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 1000 live 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 1000 live 500 后的Balance: 1000 当前Balance: 1000
You can see chaos, such as the second balance of 1,000 is withdrawn by 500 or 1,000. This competition for the same resource has a competitive condition (Race Condition
)。
Let’s see the execution results of locking:
Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 live 500 后的Balance: 1500 Balance: 1500 Pick 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 当前Balance: 1000
It's normal after locking.
The following is a more detailed mutex lock:Read/write mutex RWMutex.
2. Read and write lock RWMutex
1. Introduction to RWMutex
Read/write mutex lockRWMutex
It contains read locks and write locks, which protect the "read operations" and "write operations" of shared resources respectively.Lock methods and
Unlock
Methods are used to lock and unlock the write lock, and itsRLock
Methods andRUnlock
The methods are used to lock and unlock the read lock respectively.
With Mutex, why do you still need to read and write locks? Because in many concurrent operations, concurrent read accounts for a large proportion and write operations are relatively small, read and write locks can be read concurrently, which can provide service performance.A read and write lock has the following characteristics:
Read and write lock | Read lock | Write lock |
---|---|---|
Read lock | Yes | No |
Write lock | No | No |
That is to say,
- If a shared resource is protected by read and write locks, other
goroutine
Cannot perform write operations. In other words, read and write operations and write operations cannot be executed in parallel, that is, read and write mutually exclusive; - When protected by a read lock, multiple read operations can be performed simultaneously.
When using read and write locks, you also need to pay attention to:
- Do not unlock unlocked read and write locks;
- Cannot use write lock to unlock for read locks
- Cannot use read lock to unlock for write locks
2. RWMutex usage example
Rewrite the previous withdrawal and deposit operations and add the method to query the balance:
package main import ( "fmt" "sync" ) // account represents the counter.type account struct { num uint // Number of operations balance int // Balance rwMu * // Read and write lock} var sign = make(chan struct{}, 15) //Channel, used to wait for all goroutines // View balance: Use read lockfunc (c *account) check() { defer func() { sign <- struct{}{} }() () defer () ("The balance after %d operations: %d\n", , ) } // Save money: write lockfunc (c *account) deposit(value int) { defer func() { sign <- struct{}{} }() () defer () ("Balance: %d\n", ) += 1 += value ("Balance after saving %d: %d\n", value, ) () } // Withdraw money: write lockfunc (c *account) withdraw(value int) { defer func() { sign <- struct{}{} }() () defer () ("Balance: %d\n", ) += 1 -= value ("Balance after %d: %d\n", value, ) () } func main() { c := account{0, 1000, new()} for i:=0; i < 5; i++ { go (500) // Get 500 go (500) // Save 500 go () } for i := 0; i < 15; i++ { <-sign } ("The balance after %d operations: %d\n", , ) }
Execution results:
Balance: 1000 Pick 500 后的Balance: 500 1 次操作后的Balance: 500 1 次操作后的Balance: 500 1 次操作后的Balance: 500 1 次操作后的Balance: 500 1 次操作后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 live 500 后的Balance: 1500 Balance: 1500 Pick 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 Balance: 1000 Pick 500 后的Balance: 500 Balance: 500 live 500 后的Balance: 1000 10 次操作后的Balance: 1000
The difference between read and write locks and mutex locks is that read and write locks separate read operations and write operations on shared resources, which can achieve more complex access control.
Summarize:
A read and write lock is also a mutex, which is an extension of a mutex.Pay attention to when using:
- Must unlock after locking
- Do not repeatedly lock or unlock
- Unlocked unlocked locks
- Don't pass mutex locks
This is the article about the mutex Mutex and read-write lock RWMutex for Go concurrent programming. For more related Go language Mutex RWMutex, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!