1. Mutex
1.1 Introduction to Mutex
Go
The language synchronization tool is mainly composed ofsync
Package provided, mutex (Mutex
) and read and write lock (RWMutex
) that issync
Method in 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. Mainly including locking (Lock
Method) and unlock (Unlock
Method) Two operations, first of all, thegoroutine
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.
1.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 main goroutine wait for the other goroutines to run. Each child goroutine sends an element to the channel before the run ends.
goroutine
At the end, the element received from this channel is received the same number of sub-goroutines. After receiving, the main goroutine will be exited.
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
Balance after depositing 500: 1500
Balance: 1000
Balance after withdrawing 500: 1000
Balance: 1000
Balance after depositing 500: 1500
Balance: 1000
Balance after withdrawing 500: 1000
Balance: 1000
Balance after depositing 500: 1500
Balance: 1000
Balance after withdrawing 500: 1000
Balance: 1000
Balance after withdrawing 500: 500
Balance: 1000
Balance after depositing 500: 1000
Balance: 1000
Balance after withdrawing 500: 500
Balance: 1000
Balance after depositing 500: 1000
Current balance: 1000
You can see chaos, such as the second time the balance of 1,000 is withdrawn by 500 or 1,000, and there is a race condition for this competition for the same resource.
Let’s see the execution results of locking:
Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after depositing 500: 1500Balance: 1500
Balance after withdrawing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Current balance: 1000
It's normal after locking.
The following is a more detailed mutex: Read/write mutexRWMutex
。
2. Read and write lock RWMutex
2.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.of type
Lock
Methods andUnlock
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 lockMutex
, why do you still need a read and write lock? 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.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
Balance after withdrawing 500: 500Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after depositing 500: 1500Balance: 1500
Balance after withdrawing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance: 1000
Balance after withdrawing 500: 500Balance: 500
Balance after depositing 500: 1000Balance after 10 operations: 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 details of Go concurrent programming and mutex locks. For more related Go concurrent programming and mutex locks, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!