Recently, WHEN I was working on a project with Golang, I encountered the problem of coroutine concurrency control, so I studied the content of coroutine in detail and learned it from the following points:
What is a coroutine
The underlying implementation of coroutines
Concurrency control of coroutines
What is a coroutine
Remember when learning the operating system process and thread, by the process and thread comparison to learn the difference and connection between the two, so you might as well learn the difference and connection between the process, thread and coroutine:
-
Threads and processes are scheduled by the OS, with the concept of CPU time slices and a variety of scheduling algorithms
-
Coroutines, also known as user-level threads, are transparent to the kernel, completely controlled by the user, and are not scheduled by the OS, so there is no scheduling algorithm to quietly switch between coroutines
I think the above comparison can be a good understanding of coroutines and its advantages. In the past, when writing programs in Java, we always made a thread pool to control concurrency, in order to prevent excessive system overhead and improve system efficiency, because threads are at the OS level and their scheduling completely depends on the OS. Every time to switch threads, the OS needs to switch from user mode to kernel mode, which is inefficient. Golang is designed for concurrency, and user-level threading means that coroutines don’t need the OS to do context switching
In Golang, goroutine is essentially a coroutine. To be more specific, Goroutine can be understood as a coroutine encapsulated and processed by Golang in multiple aspects such as runtime and call. When a long execution or system call is made, Golang actively transfers the CPU (P) of the current Goroutine so that other Goroutines can be scheduled and executed, which means that Golang supports coroutines at the language level. Golang features language native support for coroutines, which can be created by adding the go keyword in front of a function or method
For details of the goroutine scheduler implementation mechanism, see the GPM model
The underlying implementation of coroutines
It works in the same way as threads. When thread A switches to thread B, the execution progress of thread A needs to be pushed onto the stack, and then the execution progress of thread B is pushed off the stack and entered the execution sequence of thread B. Coroutines simply implement this at the application layer. However, coroutines are not scheduled by the operating system, and applications do not have the ability or permission to perform CPU scheduling. How to solve this problem?
The answer is that coroutines are thread-based. Internally, a set of data structures and n threads are maintained. The actual execution is still threads, and the code executed by the coroutine is thrown into a queue to be executed and pulled out by n threads. This solves the problem of coroutine execution.
So how do coroutines switch? The answer is: Golang encapsulates various IO functions that the application uses to call the operating system’s asynchronous IO functions. When these asynchronous functions return busy or bloking, Golang takes advantage of this opportunity to push the existing execution sequence. The basic principle of having a thread pull another coroutine code to execute is to take advantage of and encapsulate the asynchronous functions of the operating system. Including Linux epoll, Select, Windows IOCP, event, and so on.
Since Golang implements coroutines from multiple levels of compiler and language base library, golang’s coroutine is the most complete and mature language with coroutine concept. 100,000 coprograms running at the same time without pressure. The point is, we don’t code this way. But in general, programmers can write Golang code with more focus on the implementation of business logic and less effort on these key building blocks.
Concurrency control of coroutines
If we want to perform a coroutine that has operations such as accessing a database, we have to control the concurrency of the coroutine, otherwise we may have hundreds of thousands of requests falling on the database and causing a crash. One way to control concurrency is through channels
Let’s take a look at an example:
Suppose we want to read uins from a slice with 100,000 uIns and query the user information for each UIN
for _,v := range Uins{
// Query the corresponding user information for each UIN
go getUserInfo(v)
}
Copy the code
If we do that, we’ll probably crash the database, so let’s go to channel
// Create an empty structure channel with size 512
channel := make(chan struct{},512)
for_,v:= range Uins{
channel <- struct{} {}go getUserInfo(v,channel)
}
func getUserInfo(v int,channel chan struct{}){
defer func(a) {
<-channel
}()
/**
业务逻辑
**/
}
Copy the code
This controls the concurrency of the coroutine. When the cahnnel is full of empty structures, the program blocks, waiting to run out
However, this may result in the last set of coroutines not being able to complete, because once the remaining capacity in the channel is greater than the number of tasks remaining, there is no blocking and the main function ends directly. To ensure that all coroutines are completed, we need to introduce sync.waitgroup control
func main(a){
// Create an empty structure channel with size 512
channel := make(chan struct{},512)
/ / create a sync. WaitGroup
var wg sync.WaitGroup
// Wg is the length of Uins
wg.Add(len(Uins))
for_,v:= range Uins{
channel <- struct{} {}// Pass WG as a parameter to the coroutine
go getUserInfo(v,channel,&wg)
}
// block until wg is 0
wg.Wait()
return
}
func getUserInfo(v int,channel chan struct{},wg *sync.WaitGroup){
defer func(a) {
// Wargaming is reduced by 1 before return
wg.Done()
<-channel
}()
/**
业务逻辑
**/
}
Copy the code