“This is the 15th day of my participation in the First Challenge 2022. For details: First Challenge 2022”
Large programs usually consist of many smaller subroutines. For example, a Web server processes a request from a Web browser and provides an HTML Web page in response. Each request is handled like a small program. For programs like this, it is ideal to be able to run their small components (in the case of a network server, handling multiple requests) at the same time. Making progress on more than one task at a time is called concurrency.
thread
Threading is a feature provided to you by the operating system that lets you run parts of a program in parallel. Suppose your program consists of two main parts, Part 1 and Part 2, and you write code such that part 1 runs on thread one and Part 2 runs on thread two. In this case, both parts of the program run in parallel; The image below illustrates what it looks like:
There is a gap between the number of truly independent threads in modern software and the amount of concurrent software a program needs to execute. In modern software, you may need thousands of programs running independently at the same time, even though your operating system may only provide four threads!
What is concurrency
And can perform multiple tasks at the same time. Concurrent programming has a wide range of meanings, including multi-threaded programming, multi-process programming and distributed programs.
In Go, concurrency means that your program is able to cut itself into smaller parts and then be able to run separate parts at different times, with the goal of performing all tasks as quickly as possible based on the number of resources available.
Goroutines and channels are used in Go to support concurrency.
Goroutines
A goroutine is a function that can be run in parallel with other functions. Run concurrently with other functions. To create a goroutine we use the keyword go, followed by a function call:
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main(a) {
go f(0)
var input string
fmt.Scanln(&input)
}
Copy the code
The program consists of two Goroutines. The first goroutine is implicit and is the main function itself. The second Goroutine is created when we call go f(0). Normally, when we call a function, our program executes all the statements in the function and then returns to the next line after the call. With Goroutine, we immediately return to the next line instead of waiting for the function to complete. That’s why Scanln is called; Without it, the program will exit before it has a chance to print all the numbers.
Goroutines are lightweight and we can easily create thousands of Goroutines. We can modify our program to run 10 goroutines by doing this:
func main(a) {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
Copy the code
You may have noticed that when you run this program, it seems to run Goroutine sequentially rather than simultaneously. Let’s add some delay to the function using time.Sleep and rand.intn:
package main
import (
"fmt"
"math/rand"
"time"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}
func main(a) {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
Copy the code
F Prints out numbers from 0 to 10 and waits between 0 and 250 milliseconds after each number. These programs should now run simultaneously.
Channels
A channel provides a means of communication between two Goroutines and synchronizes their execution. Here is an example program that uses channels:
The program will always print “ping” (press Enter to stop). The channel type is represented by the keyword chan, followed by the type of the thing passed on the channel (in this case, we’re passing a string). The <- (left arrow) operator is used to send and receive messages in a channel.
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"}}func printer(c chan string) {
for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)}}func main(a) {
var c chan string = make(chan string)
go pinger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Copy the code
C <- “ping” means to send “ping”.
MSG := < -c means to receive a message and save it to MSG.
The FMT line can also be written as follows: FMT.Println(<-c), so MSG := <-c can be deleted.
Two Goroutines can be synchronized using such a channel. When the Pinger tries to send a message on the channel, it waits for Printer to be ready to receive the message. (This is called blocking.) Let’s add another sender to the program and see what happens. Add this feature:
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"}}func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong"}}func printer(c chan string) {
for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)}}func main(a) {
var c chan string = make(chan string)
go pinger(c)
go ponger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Copy the code
The program will now alternate printing “ping” and “pong.”
The channel direction
We can specify a direction on the channel type to restrict it to send or receive. For example, pinger’s function signature could look like this:
func pinger(c chan<- string)
Copy the code
At this point, c can only send, and trying to receive from C will cause a compilation error. Similarly, we can change printer:
func printer(c <-chan string)
Copy the code
A channel without these restrictions is called a bidirectional channel. A bidirectional channel can be passed to a function that takes a send-only or receive-only channel, but not vice versa.
Select
Go has a special statement called SELECT that works like switch, but only for channels:
package main
import (
"fmt"
"time"
)
func main(a) {
c1 := make(chan string)
c2 := make(chan string)
go func(a) {
for {
c1 <- "from c1 "
time.Sleep(2 * time.Second)
}
}()
go func(a) {
for {
c2 <- "from c2 "
time.Sleep(3 * time.Second)
}
}()
go func(a) {
for {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}()
var input string
fmt.Scanln(&input)
}
Copy the code
The program prints “From C1” every 2 seconds and “from C2” every 3 seconds. Select selects the first ready channel and receives from (or sends to) it. If more than one channel is ready, it randomly selects the channel to receive. If no channel is ready, the statement blocks until one is available.
$ go run main.go from c2 from c1 from c1 from c2 from c1 from c2 from c1 from c1 from c2 from c1 from c2 from c1 from c1 from c2Copy the code
Select statements are commonly used to implement timeouts:
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")}Copy the code
Time.after Creates a channel on which the current time will be sent After a given duration. (We’re not interested in time, so we didn’t store it in a variable.) We can also specify a default:
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")}Copy the code
By default, if none of the channels are ready.
Buffer channel
You can also pass the second argument to make when creating a channel:
c := make(chan int.1)
Copy the code
This will create a buffer channel of capacity 1. Usually channels are synchronous; Both sides of the channel will wait until the other side is ready.
The buffer channel is asynchronous; Messages will not wait to be sent or received unless the channel is full.
conclusion
This article gives a brief introduction to concurrency, and then introduces the concepts and applications of Goroutines and Channels that are native to Go. In the next article, we will learn about Go concurrency in more depth.