1. Introduction
The main content of this article is to introduce the Mutex concurrency primitive in Go. Contains basic use of Mutex, precautions for use, and some practical suggestions.
2. Basic use
2.1 Basic definition
Mutex is a synchronous primitive in Go language, with its full name Mutual Exclusion, which is a mutex. It can achieve mutually exclusive access to shared resources in concurrent programming, ensuring that only one coroutine can access shared resources at the same time. Mutex is often used to control access to critical sections to avoid the occurrence of race conditions.
2.2 How to use
The basic method of using Mutex is very simple. You can obtain the lock by calling Mutex's Lock method, and then release the lock through the Unlock method. The example code is as follows:
import "sync" var mutex func main() { () // Get the lock // Perform the operation that needs to be synchronized () // Release the lock}
2.3 Use examples
2.3.1 Example of mutex synchronization code not used
Here is a code example that uses goroutine to access shared resources but does not synchronize with Mutex:
package main import ( "fmt" "time" ) var count int func main() { for i := 0; i < 1000; i++ { go add() } (1 * ) ("count:", count) } func add() { count++ }
In the above code, we started 1000 goroutines, and each goroutine calls the add() function to add the value of the count variable by 1. Since the count variable is a shared resource, race conditions will occur when multiple goroutines are accessed simultaneously. However, since Mutex is not used for synchronization, the value of count cannot be accumulated correctly, and the final output result will also be errors.
In this example, since multiple goroutines access the count variable at the same time without synchronous control, each goroutine may read the same count value and perform the same accumulation operation. This will result in the final output count value not the desired result. If we use Mutex for synchronization control, we can avoid the occurrence of this race condition.
2.3.2 Use mutex to solve the above problems
Here is an example of using Mutex for synchronization control to solve the race condition problem in the above code:
package main import ( "fmt" "sync" "time" ) var ( count int mutex ) func main() { for i := 0; i < 1000; i++ { go add() } (1 * ) ("count:", count) } func add() { () count++ () }
In the above code, we define a variable of type mutex globally for synchronous control. In the add() function, we first call the() method to obtain the lock of mutex to ensure that only one goroutine can access the count variable. Then add 1 operation, and finally call the () method to release the lock of mutex so that other goroutines can continue to access the count variable.
By using Mutex for synchronization control, we avoid the occurrence of race conditions and ensure the correct accumulation of the count variable. The final output also meets expectations.
3. Precautions for use
3.1 Lock/Unlock needs to appear in pairs
Here is an example of code that does not appear in pairs of Lock and Unlock:
package main import ( "fmt" "sync" ) func main() { var mutex go func() { () ("goroutine1 locked the mutex") }() go func() { ("goroutine2 trying to lock the mutex") () ("goroutine2 locked the mutex") }() }
In the above code, we create a variable mutex of type, and then use this mutex in both goroutines.
In the first goroutine, we call the() method to obtain the lock of mutex, but we do not call the corresponding Unlock method. In the second goroutine, we first print a message, and then call the() method to try to acquire the lock of the mutex. Since the first goroutine does not release the mutex lock, the second goroutine is blocked in the Lock method and cannot be executed.
Therefore, in the process of using Mutex, it is necessary to ensure that each Lock method has a corresponding Unlock method to ensure the normal use of Mutex.
3.2 The used Mutex cannot be passed as a parameter
Here is an example of the code that has been passed as a parameter using Mutex:
type Counter struct { Count int } func main(){ var c Counter () defer () ++ foo(c) ("done") } func foo(c Counter) { () defer () ("foo done") }
When a mutex is passed to a function, the expected behavior should be that the function can correctly obtain and release the mutex when accessing shared resources protected by mutex to avoid the occurrence of race conditions.
If we copy this Mutex without unlocking, it will cause the lock to fail. Because Mutex's status information has been copied, the copied Mutex is still in a locked state. In the function, when accessing critical area data, the first thing to do is to call the method to lock, and the incoming Mutex is actually in a locked state, and the function will never be able to obtain the lock.
Therefore, the used Mutex cannot be passed directly as a parameter.
3.3 The Lock/UnLock method cannot be called repeatedly
Here is an example where the same Mutex is repeatedly locked:
package main import ( "fmt" "sync" ) func main() { var mu () ("First Lock") // Repeat lock () ("Second Lock") () () }
In this example, we first locked Mutex once, and then performed another lock operation without unlocking.
In this case, the program will have a deadlock because the second locking operation has been blocked, waiting for the first locking unlock operation, and the first locking unlock operation is also blocked, waiting for the second locking unlock operation, resulting in a situation of waiting for each other and being unable to continue execution.
Mutex is actually implemented through an int32 type flag bit. When this flag is 0, it means that the Mutex has not been retrieved by any goroutine; when the flag is 1, it means that the Mutex has been retrieved by a goroutine.
Mutex's Lock method actually changes this flag bit from 0 to 1, indicating that the lock has been acquired; the Unlock method changes the flag bit from 1 to 0, indicating that the lock has been released. When the Lock method is called the second time, the mark bit is 1, which means that there is a goroutine holding the lock and it will be blocked at this time. The one holding the lock is actually the current goroutine, and the program will be blocked forever.
4. Practical advice
4.1 Mutex locks do not protect two unrelated data at the same time
Here is an example of using Mutex to protect two unrelated data at the same time
// net/http type Transport struct { lk idleConn map[string][]*persistConn altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper } func (t *Transport) CloseIdleConnections() { () defer () if == nil { return } for _, conns := range { for _, pconn := range conns { () } } = nil } func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) { if scheme == "http" || scheme == "https" { panic("protocol " + scheme + " already registered") } () defer () if == nil { = make(map[string]RoundTripper) } if _, exists := [scheme]; exists { panic("protocol " + scheme + " already registered") } [scheme] = rt }
In this example, idleConn stores idle connections, altProto is the processor that stores protocols, CloseIdleConnections method closes all idle connections, and RegisterProtocol is used to register protocol processing.
Although the two parts of the data of ideConn and altProto are not related, they are protected by the same Mutex. In this way, when the RegisterProtocol method is called, the CloseIdleConnections method cannot be called, which will lead to too much competition and affect performance.
Therefore, in order to improve concurrency performance, the locking granularity of Mutex should be minimized as much as possible and only the data that needs to be protected should be protected.
Transport has been improved in modern versions of net/http, using different mutexes to protect idleConn and altProto respectively to improve performance and code maintainability.
type Transport struct { idleMu idleConn map[connectMethodKey][]*persistConn // most recently used at end altMu // guards changing altProto only altProto // of nil or map[string]RoundTripper, key is URI scheme }
4.2 Suggestions for position placement in Mutex embedded structure
Embed Mutex into a structure, if you only need to protect some of the data, you can place Mutex on the fields you need to control, and then use spaces to separate the protected fields from other fields. This allows for finer granular locking, and more clearly expressing the intention that each field needs to be protected mutually exclusively, making the code easier to maintain and understand. Here are some practical examples:
ReqLock in the Server structure is used to protect the freeReq field, and respLock is used to protect the freeResp field. Mutex is placed on the protected field.
//net/rpc type Server struct { serviceMap // map[string]*service reqLock // protects freeReq freeReq *Request respLock // protects freeResp freeResp *Response }
In the Transport structure, the idleMu lock will protect a series of fields such as closeIdle. At this time, the lock is placed at the top of the protected field, and then separate the fields protected by the idleMu lock from other fields with spaces. Achieve finer granular locking can also more clearly express the intent of each field that needs to be mutually exclusive protection.
// net/http type Transport struct { idleMu closeIdle bool // user has requested to close all idle conns idleConn map[connectMethodKey][]*persistConn // most recently used at end idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns idleLRU connLRU reqMu reqCanceler map[cancelKey]func(error) altMu // guards changing altProto only altProto // of nil or map[string]RoundTripper, key is URI scheme connsPerHostMu connsPerHost map[connectMethodKey]int connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns }
4.3 Minimize the scope of lock
In a code segment, minimizing the scope of the lock can improve concurrency performance, reduce the waiting time of the lock, and thus reduce the waste of system resources.
The larger the scope of the lock, the more code there is to wait for the lock, which will reduce concurrency performance. Therefore, when writing code, the scope of the lock should be minimized as much as possible and only locks should be added in the critical area that needs protection.
If the scope of the lock is the entire function, usedefer
It is a common practice to release locks by statements, which can avoid problems such as deadlocks caused by forgetting to manually release the lock.
func (t *Transport) CloseIdleConnections() { () defer () if == nil { return } for _, conns := range { for _, pconn := range conns { () } } = nil }
When using a lock, be careful to avoid executing long-running code or IO operations in the lock, because this will block the use of the lock and cause the lock to wait time to become longer. If you really need to execute long-running code or IO operations in the lock, you can consider releasing the lock, allowing other codes to be executed first, and then reacquisition the lock, such as the following code example
// net/http/httputil func (cc *ClientConn) Read(req *) (resp *, err error) { // Retrieve the pipeline ID of this request/response pair () id, ok := [req] delete(, req) if !ok { () return nil, ErrPipeline } () // xxx omits some intermediate logic // Read http response data from the http connection. This IO operation is unlocked first resp, err = (r, req) // The network IO operation is over, and then continue reading () defer () if err != nil { = err return resp, err } = ++ if { = ErrPersistEOF // don't send any more requests return resp, } return resp, err }
5. Summary
In concurrent programming, Mutex is a common synchronization mechanism used to protect shared resources. In order to improve concurrency performance, we need to minimize the lock granularity of Mutex as much as possible, protect only the data that needs to be protected, and at the same time, in a code segment, minimize the scope of the lock. If the scope of the lock is the entire function, you can use defer to unlock it when the function exits. When Mutex is embedded in a structure, we can put the Mutex on the field we want to control and separate the fields with spaces so that only the data we need to protect is protected.
The above is the detailed explanation of GO's use of Mutex to ensure the correctness of concurrent programs. For more information on the correctness of GO Mutex, please pay attention to my other related articles!