1. Introduction
Hello everyone, today I will introduce one of the fairly important topics in Golang, which is Concurrency. So does concurrency in Golang have new concepts, how do they work and how to use them? We will learn and answer together through this article
2. Concepts
Before going into specific examples, we will learn through new concepts related to Concurrency in Golang.
2.1. Goroutine
When it comes to concurrency, Golang gives us a new concept, which is Goroutine. So what is Goroutine?
Goroutine is a lightweight gadget managed by the Go runtime. Goroutine is a function that can run concurrently with other functions.
Syntax using Goroutine
1 2 | go myFunction(myParam) |
2.2. Channel
The question arises, so how can goroutines communicate with each other? The answer here is that channel
Channels provide a way for goroutines to communicate with each other and synchronize their execution.
By default, the channel is bidirectional, which means goroutines can send or receive data across the same channel
The syntax for using channel
- Declare 1 channel:
1 2 | myChannel := make(chan int) |
- Send an element into the channel:
1 2 | mychannel <- element |
- Get the element from the channel:
1 2 | element := <-mychannel |
- In the case the result of the received statement will not be used is also a valid statement. You could also write a receive statement like this:
1 2 | <-Mychannel |
3. Use Goroutine
Let’s start with a basic example when not using Goroutine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package main import "fmt" import "time" func main() { hello("John") hello("Peter") } func hello(name string) { for i := 0; i < 5; i++ { fmt.Println("Hello", name) time.Sleep(time.Millisecond * 500) } } |
It is easy to see that we have a function hello
to print the greeting 5 to a specific name 0.5s apart, the main function will send a greeting to 2 names John & Peter
The result, we will get the following:
1 2 3 4 5 6 7 8 9 10 11 | Hello John Hello John Hello John Hello John Hello John Hello Peter Hello Peter Hello Peter Hello Peter Hello Peter |
OK, so now let’s change to add goroutine to the two hello
commands in the main function. Change the main function as follows:
1 2 3 4 5 | func main() { go hello("John") go hello("Peter") } |
When we run again you will see our program finished without printing anything. Why so?
The simple answer here is: in Go, when the main function ends, the program also ends, then all goroutines also end. In the above example, after running 2 goroutine commands, the program terminates so 2 goroutine has no time to run. Let’s adjust a little bit, we will sleep the main function again a bit and then check again with the following command at the end of the main function:
1 2 | time.Sleep(time.Second * 3) |
Oh, we get the following results immediately:
1 2 3 4 5 6 7 8 9 10 11 | Hello John Hello Peter Hello Peter Hello John Hello Peter Hello John Hello John Hello Peter Hello Peter Hello John |
It can be seen that the sentences “Hello John” & “Hello Peter” are displayed alternately, proving that the above 2 goroutines have been run asynchronously as expected.
Channel
Now we will also go to channel examples – how goroutines communicate with each other.
Let’s write a simple example as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package main import ( "fmt" "time" ) func main() { c := make(chan int) go process(c) for { fmt.Println("Received:", <-c) } } func process(c chan int) { for i := 1; i <= 3; i++ { c <- i time.Sleep(time.Millisecond * 300) } } |
Looking at the above example we can see that the main
function initializes a channel of type int then calls the process
method to send 3 values to the channel. Finally, use the for function to print out the values received by the channel.
Running the above code together, we will get the following output:
1 2 3 4 5 6 7 8 9 | Received: 1 Received: 2 Received: 3 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /tmp/sandbox708494458/prog.go:13 +0x7d |
Oh, the 3 values received are correct, but why is there a deadlock error?
Well, the reason is that the process
method after sending 3 values to the channel ends, while the main
function is still waiting for the next value to be sent into the channel, but nothing can send any more to the channel. This results in the main function being wait forever … However, go can detect this problem (runtime problem, not compile time) and stop the program.
To handle this problem, very simply, we close the channel by adding the following close(c)
command at the end of the process
method and re-edit the loop that prints out the results received in the channel as follows:
1 2 3 4 5 6 7 8 | for { i, open := <-c if !open { return } fmt.Println("Received:", i) } |
=> So every time we receive a value from the channel, we check before that channel has been closed. If closed, we will stop the program. Then the program will end normally and the results will be as follows:
1 2 3 4 5 6 | Received: 1 Received: 2 Received: 3 Program exited. |
Or more simply, we just need to write the loop like this:
1 2 3 4 | for i := range c { fmt.Println("Received:", i) } |
Buffered Channels
Next we come up with a simpler example, create a channel, then send the value in and get the following:
1 2 3 4 5 6 7 8 9 10 | package main import "fmt" func main() { ch := make(chan int) ch <- 1 fmt.Println("Received:", <-ch) } |
The following results:
1 2 3 4 5 6 | fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox980511399/prog.go:7 +0x59 |
Why so? We would expect the result to show a normal value of 1. However, here could be explained as follows:
When we initialize the channel with the command ch := make(chan int)
, the mechanism here is that every time we send a value into the channel, goroutine will block until the value in that channel is retrieved. . In this case, the main program will be blocked and not able to reach the underlying value line. And so, Go discovers the problem of being wait forever and resolves the deadlock error again.
In case we send into the channel with 1 goroutine & receive channel in another goroutine, it can still work normally, the sending goroutine will block until goroutine receives value in the output channel, then the sending goroutine continues. work.
If you want to send multiple values into the channel then there is that way. Go introduces a form of Buffered Channels
. With this channel we can specify the size of the channel. Let’s edit the code as follows:
1 2 3 4 5 6 7 8 | func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println("Received:", <-ch) fmt.Println("Received:", <-ch) } |
Using the command make(chan int, 2)
means we have created a Buffered Channel with size = 2. Using this channel we will no longer have to worry about goroutine being blocked when sending message. The following results:
1 2 3 4 5 | Received: 1 Received: 2 Program exited. |
Of course if we intentionally send values more than the size of the channel, a deadlock occurs.
Selection
Moving on, let’s go to the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go sendAndSleep(c1, "Sleep 1s", time.Second*1) go sendAndSleep(c2, "Sleep 5s", time.Second*5) for { fmt.Println("Received:", <-c1) fmt.Println("Received:", <-c2) } } func sendAndSleep(c chan string, value string, duration time.Duration) { for { c <- value time.Sleep(duration) } } |
We create 2 channels & 2 goroutine, goroutine 1 will send the message “Sleep 1s” every 1 second, goroutine 2 will send the message “Sleep 5s” every 5s, then we print out the mesages received in channel 1 & 2. When trembling, we have the following results:
1 2 3 4 5 6 7 8 | timeout running program Received: Sleep 1s Received: Sleep 5s Received: Sleep 1s Received: Sleep 5s Received: Sleep 1s ... |
This is a timeout running program because we send in & out forever. However, let’s just ignore this problem and look at the results, we will see, even though goroutine 1 sends channel 1 message every 1s. But the main function has to wait for the message to be printed on channel 2 before it can continue to print the message in channel 1. That is why channel 1 is received every 1 second but prints out sequentially with channel 2 as 5s.
In order not to let channel 1 wait for channel 2 to receive further processing, we can use the select command in the for function as follows:
1 2 3 4 5 6 7 8 9 | for { select { case v1 := <-c1: fmt.Println("Received:", v1) case v2 := <-c2: fmt.Println("Received:", v2) } } |
In this way we can receive the message channel 1 every time we have, without having to wait for channel 2 like the code above. And this is the result:
1 2 3 4 5 6 7 8 9 10 11 12 | timeout running program Received: Sleep 5s Received: Sleep 1s Received: Sleep 1s Received: Sleep 1s Received: Sleep 1s Received: Sleep 1s Received: Sleep 5s Received: Sleep 1s Received: Sleep 1s ... |
Conclude
Thus, through the above example I have introduced about concurrency in Golang, but specifically here is how goroutine works, how to use channel, select, … in Golang. These are just the fairly basic parts of concurrency in Golang, but hopefully this article can help you understand and apply in learning as well as work. Thank you for watching the article!