1, Introduction to goroutine Foundation
goroutine is a coroutine in Golang. It is a micro thread, which consumes less resources than threads. The function of thread is to carry out concurrency or parallelism, and make full use of the resources of computer multi-core.
- Concurrent multiple tasks run on one cpu, only one task is processed at a certain time, and the time of switching back and forth between tasks is very short
- Parallel multiple tasks run on multiple CPUs, and multiple CPUs process multiple tasks at a certain time to achieve the effect of parallel
So how to use goroutine in Golang? Multiple coprocesses can be started in the main thread of Golang. For example, there is a case:
- The main thread starts a coroutine to output "Hello goroutine" every 1 second and outputs it 10 times
- The main thread outputs "Hello main" every 1 second and outputs it 10 times
- The main thread and the coroutine work at the same time
package main import ( "fmt" "time" ) func printGoroutine() { // Co process execution function for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello goroutine %v \n", i) } } func main() { // Main thread // Start a collaborative process go printGoroutine() // Continue to execute the main process code for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello main %v \n", i) } } /* Output: Hello main 0 Hello goroutine 0 Hello goroutine 1 Hello main 1 Hello main 2 Hello goroutine 2 Hello goroutine 3 Hello main 3 Hello main 4 */
You can see that the main thread and the coroutine work at the same time. The flow chart is as follows:
- Once the go command is passed, it is equivalent to starting a co process, which is synchronized with the main thread, and it is equivalent to starting another branch in the main thread
- If the main thread ends, other coroutines will end even if they have not finished executing
- A coroutine can also complete its own task before the end of the main thread
- Thread startup directly affects the physical level cpu, which is very resource-consuming, and the collaborative process can easily control millions of levels
2, Synchronization
(1) WaitGroup
An obvious problem in the above example is that printGoroutine should be printed five times, but it is obviously missing one time. Why? In fact, it is obvious in the figure that if the main running in the main collaboration process ends, the rest of the collaboration process will be terminated, so how to terminate the main collaboration process after all the collaboration processes are completed is a very important thing.
sync.WaitGroup can solve this problem by stating:
var wg sync.WaitGroup
After that, the wg variable can be used normally. This type has three pointer methods, Add, Done and Wait
sync.WaitGroup is a structure type with a count field inside. When sync After a variable of waitgroup type is declared, when the value of this field is 0, you can increase or decrease the count value through the Add method. And this value is the number of started processes.
The Wait method is used to block the goroutine calling it until the count value is 0.
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup func printGoroutine() { // Co process execution function defer wg.Done() // Every time the process is executed, Add Parameter in minus 1 for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello goroutine %v \n", i) } } func main() { // Running in the main coordination process main function // Start a collaborative process wg.Add(1) // Add The parameter in is the number of open processes go printGoroutine() // Continue to execute the main process code for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Printf("Hello main %v \n", i) } wg.Wait() // Block until Add The parameter in changes to 0 to end the main collaboration } /* Output: Hello main 0 Hello goroutine 0 Hello goroutine 1 Hello main 1 Hello main 2 Hello goroutine 2 Hello goroutine 3 Hello main 3 Hello main 4 Hello goroutine 4 */
You can see the output result. One more line will be output. The main process will wait until all the processes are executed.
(2) Lock mechanism
1. Problem introduction
Data security issues will be involved in the concurrent operation of multiple processes, such as:
package main import ( "fmt" "sync" ) var num int var wg sync.WaitGroup func sub() { defer wg.Done() for i := 0; i < 100000; i++ { num -= 1 } } func add() { defer wg.Done() for i := 0; i < 100000; i++ { num += 1 } } func main() { wg.Add(2) // Start two coroutines, plus 1 and minus 1 respectively go add() go sub() // At this time, wait for the execution of the two cooperation processes to be completed wg.Wait() // Then execute the logic of the main process fmt.Println(num) }
The above output result should be 0, because one function increases and one function decreases, but the result is not the expected result at all. The result of each execution is different:
PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -36503 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -60812 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -47786 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go 89516 PS D:\go_practice\go_tutorial\day19\MutexDemo01> go run .\main.go -97086
Then why? This is because of the resource competition caused by multiple collaborative processes:
Because the two processes operate on num at the same time, the number that the two processes may get is 0. At this time, the add function adds it. If it is added to 6000, and the subtraction is constantly operating on num, the obtained num may be 6000; On the contrary, the num of add may also obtain the value of the sub function, which is a negative number. Therefore, the value of num is uncontrollable. So how to solve this problem?
Locks can be used in Go, which are divided into:
- mutex
- Read write lock
2. Mutex
Mutexes control the access of shared resources in concurrent programs. They appear in pairs, i.e. Lock and Unlock. Only when a coroutine obtains the Lock can it execute the following code block.
package main import ( "fmt" "sync" ) var num int var wg sync.WaitGroup var lock sync.Mutex // Declare a mutex variable func sub() { defer wg.Done() for i := 0; i < 100000; i++ { lock.Lock() // Lock. Currently, only this collaboration can be executed num -= 1 lock.Unlock() // Unlock } } func add() { defer wg.Done() for i := 0; i < 100000; i++ { lock.Lock() // Lock when reading data, because there may be resource competition at this time num += 1 lock.Unlock() } } func main() { wg.Add(2) // Start two coroutines, plus 1 and minus 1 respectively go add() go sub() // At this time, wait for the execution of the two cooperation processes to be completed wg.Wait() // Then execute the logic of the main process fmt.Println(num) }
In the previous code, if mutex can solve the problem, why should mutex be added to the reading place? Because there may be competition for resources. Syntax of mutex:
... var lock sync.Mutex // Declare a mutex variable func main() { ... lock.Lock() // Lock. Currently, only this collaboration is executed ... Code block ... lock.Unlock() // Unlock }
The mutex needle is a write operation for the above-mentioned operations, which can safely solve this problem. However, if it is a read-write hybrid, its strength is relatively large, which is easy to affect the performance of the program. The read operation will not affect the data change, so it can be read by multiple people at the same time, and the above mutex can only be read or written by one process at the same time. Therefore, this problem can be solved by using read-write lock. Then the following example is used to demonstrate the read-write lock, reading data and writing data at the same time.
2. Read write lock
Read / write lock is a mutex for read / write operations. Its difference from ordinary mutex is that it can lock and unlock read operations and write operations respectively. It allows any read operation to be carried out at the same time, but it only allows one write operation to be carried out at the same time, and the read operation is not allowed when the write operation is carried out. in other words. Multiple write operations in a read / write lock are mutually exclusive, and write operations and read operations are mutually exclusive - but There is no mutually exclusive relationship between multiple read operations. This can greatly improve the performance of the program.
Usage syntax:
var rwlock sync.RWMutex // Declare a read-write lock variable func read() { rwlock.RLock() .... ... rwlock.RUnlock() } func write() { rwlock.Lock() .... ... rwlock.Unlock() }
The following is an example of a read-write lock:
package main import ( "fmt" "sync" "time" ) var mapNum = make(map[int]int, 50) var wg sync.WaitGroup var rwlock sync.RWMutex // Declare a read-write lock variable func write() { defer wg.Done() rwlock.Lock() fmt.Printf("Write data\n") time.Sleep(time.Second * 2) fmt.Printf("Write end\n") rwlock.Unlock() } func read() { defer wg.Done() rwlock.RLock() fmt.Printf("Read data\n") time.Sleep(time.Second) fmt.Printf("End of reading\n") rwlock.RUnlock() } func main() { // Start 10 reads and 10 writes wg.Add(22) for i := 0; i < 20; i++ { go write() } for i := 0; i < 2; i++ { go read() } // At this time, wait for the completion of 20 collaborative processes wg.Wait() } /* Write data Write end Read data Read data End of reading End of reading Write data Write end Write data Write end Write data Write end Write data */
It can be seen from the output that when writing data, write first and then end, while reading data can be multiple concurrent at the same time.