Learn what
What is the difference between concurrency and parallelism?
What is Goroutine?
What is a channel?
How does Goroutine communicate?
Use of correlation functions?
How to use the select statement?
Concurrency and parallelism
In order to explain this concept more interestingly, I borrow an answer from Zhihu:
When you're halfway through your meal and the phone comes, you don't answer it until you're finished, which means you don't support concurrency or parallelism.
When you are halfway through your meal and the phone comes, you stop and answer the phone. After that, you continue to eat, which shows that you support concurrency.
When you call halfway through your meal, you eat while you call, which shows that you support parallelism.
The key to concurrency is that you have the ability to handle multiple tasks, not necessarily at the same time.
The key to parallelism is your ability to handle multiple tasks at the same time.
Corresponding to the CPU, if it is multi-core, it has the ability to execute at the same time, that is, it has the ability of parallel.
For Go language, it arranges our code for concurrency and parallelism.
What is Goroutine
Learn this to know how to write a concurrent program. It's very simple to use. Now start.
Goroutine is a Coroutine in the Go language. It is called Coroutine in other languages. It is simply understood as a lighter thing than threads.
Besides, functions can be executed asynchronously.
main Goroutine
When the main entry function is started, a main Goroutine is automatically run in the background to restore it to everyone.
package main func main() { panic("Look here") }
Executing the above code will output the following information:
panic: Look here goroutine 1 [running]: main.main()
From the results, we can see that a word goroutine appears, and its corresponding index is 1.
Create Goroutine
Creating Goroutine is very simple. You only need to add a go keyword in front of the function. The format is as follows:
go fun1(...)
Anonymous functions are also supported.
go func(...){ // ... }(...)
- The function after the go keyword can write the return value, but it is invalid. Because Goroutine is asynchronous, it can't be accepted.
Let's look at a complete example:
package main import ( "fmt" ) func PrintA() { fmt.Println("A") } func main() { go PrintA() fmt.Println("main") }
The main function above has only two lines:
First line: create A Goroutine and asynchronously print the "A" string.
Line 2: print the "main" string.
Now stop for a moment and think about what the output will be after executing the code.
The results are as follows:
main
You're right. You didn't output the "A" string.
Because the Goroutine created by go PrintA() is executed asynchronously, the main function will not care about it when it exits the program after execution. So let's see how to make the main function wait for Goroutine to finish executing.
Method 1: use the time.Sleep function.
func main() { go PrintA() fmt.Println("main") time.Sleep(time.Second) } // output main A
Let's wait a minute before the main function exits.
Method 2: use an empty select statement. The use of non empty select will be explained together with the channel.
func main() { go PrintA() fmt.Println("main") select {} } // output main A fatal error: all goroutines are asleep - deadlock! ...
"A" string is output, but the program also has an exception.
The reason is that when there is a running Goroutine in the program, select {} will wait all the time. If the Goroutine execution ends and there is nothing to wait for, an exception will be thrown.
In a real project, if there is an exception, what is the use scenario of select {}? For example:
- The crawler project creates Goroutine, which needs to crawl data all the time without stopping.
Method 3: use WaitGroup type to wait for Goroutine to finish, which is often used in projects. The complete example is as follows:
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func PrintA() { fmt.Println("A") wg.Done() } func main() { wg.Add(1) go PrintA() wg.Wait() fmt.Println("main") }
Declare a WaitGroup type variable wg, which does not need to be initialized when used.
wg.Add(1) indicates that you need to wait for a Goroutine. If there are two, use Add(2).
When a Goroutine runs, use wg.Done() to notify.
wg.Wait() waits for Goroutine to finish executing.
Control concurrency
In Go language, you can control the number of CPU cores. Starting from Go1.5, the default setting is the total number of CPU cores. If you want to customize the settings, use the following functions:
num := 2 runtime.GOMAXPROCS(num)
num is allowed if it is greater than the number of CPU cores. The Go language scheduler will allocate many goroutines to different processors.
What is a channel
Now that you know how to create Goroutine, the next step is to know how to communicate between them.
Goroutine communication uses "channel". If Goroutine1 wants to send data to Goroutine2, put the data into the channel, and Goroutine2 can take it directly from the channel, and vice versa.
When placing data for a channel, you can also specify the type of data placed by the channel.
Create channel
When creating channels, there are two types: unbuffered and buffered.
1. No buffer
strChan := make(chan string)
Defines an unbuffered channel with a storage data type of string. If you want to store any type, the data type is set to an empty interface.
allChan := make(chan interface{})
Once the channel is created, the data will be put into the channel.
strChan := make(chan string) strChan <- "Old seedling"
Use the "< -" operator to link data, indicating that the "old seedling" string is sent to the strChan channel variable.
However, an error will be reported when putting data in this way, because strChan variable is a non buffered channel, and the main function will wait all the time when putting data, so it will cause deadlock.
If you want to solve the deadlock situation, you must ensure that there is a place in the asynchronous read channel, so you need to create a Goroutine to take charge of it.
Examples are as follows:
// concurrency/channel/main.go package main import ( "fmt" "sync" ) var wg sync.WaitGroup func Read(strChan chan string) { data := <-strChan fmt.Println(data) wg.Done() } func main() { wg.Add(1) strChan := make(chan string) go Read(strChan) strChan <- "Old seedling" wg.Wait() } // output Old seedling
The Read function reads the channel data and prints it.
Channels are reference types, so pointers are not required when passing.
< - strchan means to get data from the channel. If there is no data in the channel, it will block.
wg.Wait() waits for the Read asynchronous function to finish executing.
2. Buffered
After reading the above, you will understand that for unbuffered channels, it will cause blocking. In order not to block, you must create a Goroutine to read from the channel.
For channels with buffer, there will be room for buffer. Let's take a look at the details.
Create a buffer channel as follows:
bufferChan := make(chan string, 3)
A channel with a storage data type of string is created.
Three data can be buffered, that is, three data can be sent to the channel without blocking.
The tests are as follows:
// concurrency/bufferchannel/main.go package main import "fmt" func main() { bufferChan := make(chan string, 3) bufferChan<-"a" bufferChan<-"b" bufferChan<-"c" fmt.Println(<-bufferChan) } // output a
Store three strings into the bufferChan variable.
There is no blocking when storing 3 data. When the number of stored data exceeds 3, Goroutine needs to read asynchronously.
When to use the buffer channel, for example:
For crawler data, the first Goroutine is responsible for crawling data, and the second Goroutine is responsible for processing and storing data. When the processing speed of the first is greater than that of the second, the buffer channel can be used for temporary storage.
After being temporarily stored, the first Goroutine can continue to crawl. Unlike the unbuffered channel, it will be blocked when putting data until the channel data is read out.
In order to deepen the impression, another picture:
Illustration:
bufferChan is a buffer channel with a length of 3 and has stored 2 data.
Look at the two arrows in the figure. The arrow is on the right of bufferChan, indicating save, and the left indicates take.
Access according to the first in first out rule.
Unidirectional channel
Now you know how to create a two-way channel. A two-way channel means that you can save and retrieve.
The one-way channel is created as follows:
readChan := make(<-chan string) writeChan := make(chan<- string)
readChan can only read data.
writeChan can only access data.
But the channel created in this way cannot transfer data. Why?
Because if I can only read the channel and can't save data, I'm lonely. What's the use of the stored channel if I can't get the data out.
Now let's take a look at an example of how to correctly use a one-way channel, as follows:
// concurrency/onechannel/main.go package main import ( "fmt" "sync" ) var wg sync.WaitGroup // Write channel func write(data chan<- int) { data<-520 wg.Done() } // Read channel func read(data <-chan int) { fmt.Println(<-data) wg.Done() } func main() { wg.Add(2) dataChan := make(chan int) go write(dataChan) go read(dataChan) wg.Wait() } // output 520
Two goroutines are created. The read function is responsible for read-only and the write function is responsible for write only.
When a channel is passed, it converts a two-way channel into a one-way channel.
Traversal channel
In the actual project, a large amount of data will be generated in the channel. At this time, it is necessary to read from the channel circularly.
Now, an example of rewriting one-way channel write data:
func write(data chan<- int) { for i := 0; i < 10; i++ { data<-i } wg.Done() }
This code is to write numbers to the channel loop.
Next, read the channel data in two ways.
1. Dead cycle
func read(data <-chan int) { for { d := <-data fmt.Println(d) } wg.Done() }
Use an endless loop to read data, but there is a problem. When do you exit the for loop?
When reading the channel, the read function does not know that the data has been written. If the data cannot be read, it will be blocked all the time. Therefore, if the data is written, you need to use the close function to close the channel.
func write(data chan<- int) { // ... close(data) wg.Done() }
After closing, detection and judgment are also required when reading the channel.
func read(data <-chan int) { for { d, ok := <-data if !ok { break } fmt.Println(d) } wg.Done() }
When the ok variable is false, the channel is closed.
After the channel is closed, the ok variable will not immediately become false, but wait until the data that has been put into the channel is read.
ch := make(chan string, 1) ch <- "a" close(ch) val, ok := <-ch fmt.Println(val, ok) val1, ok1 := <-ch fmt.Println(val1, ok1) // output a true false
2. for-range
You can also use the for range statement to read channels, which is a little simpler than using an endless loop.
func read(data <-chan int) { for d := range data{ fmt.Println(d) } wg.Done() }
If you want to exit the for range statement, you also need to close the channel.
After the channel is closed, there is no need to add ok judgment. After the channel data is read, it will exit automatically.
Channel function
Use the len function to get the number of unread messages in the channel, and the cap function to get the buffer size of the channel
ch := make(chan int, 3) ch<-1 fmt.Println(len(ch)) fmt.Println(cap(ch)) // output 1 3
select statement
The function of empty select statement has been known above. Now let's look at the usage of non empty select.
The select statement is similar to the switch statement. It also has a case branch and a default branch, but the select statement has two differences:
The case branch can only be a "read channel" or "write channel". If the read and write is successful, that is, it is not blocked, then the case branch is satisfied.
The fallthrough keyword cannot be used.
1. No default branch
The select statement selects a read-write successful channel in the case branch.
Correct example:
// concurrency/select/main.go package main import "fmt" func main() { ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch1 <- 1 select { case v, ok := <-ch1: if ok { fmt.Println("ch1 passageway", v) } case v, ok := <-ch2: if ok { fmt.Println("ch2 passageway", v) } } } // output ch1 Channel 1
ch1 channel has data, so it enters the first case branch.
Here we show the read channel and write data to the channel, for example: case CH2 < - 2.
If ch1 < - 1 is deleted, the select statement will wait in the main function, resulting in a deadlock.
fatal error: all goroutines are asleep - deadlock! goroutine 1 [select]: main.main() C:/workspace/go/src/gobasic/cocurrency/select/main.go:9 +0xe7
2. There is a default branch
To prevent deadlock in the select statement, the default branch can be added. This means that when there is no case branch for channel reading and writing, the default branch is used.
// ... func main() { ch1 := make(chan int, 1) ch2 := make(chan int, 1) select { case v, ok := <-ch1: if ok { fmt.Println("ch1 passageway", v) } case v, ok := <-ch2: if ok { fmt.Println("ch2 passageway", v) } default: fmt.Println("No read / write channel") } } // output No read / write channel
summary
This class is very key and prone to problems. I will emphasize the key points:
Add the go keyword before the function call to create Goroutine.
When Goroutine is executed, it will not wait synchronously. The commonly used type is WaitGroup.
Goroutine's communication uses channel transmission.
For unbuffered channels, do not read or write synchronously, otherwise it will be blocked.
Finally, try to figure out another sentence: don't use shared memory to communicate, use communication to share memory.