This is the 14th day of my participation in the August More Text Challenge. For details, see: August More Text Challenge

Finally, at the heart of Go, concurrent programming. There are several more chapters due to the large amount of content.

11.1 an overview of the

11.1.1 Parallelism and Concurrency

Parallel: The simultaneous execution of multiple instructions on multiple processors at the same time.

Concurrency: One instruction at a time can only be executed, but multiple process instructions are executed in rapid rotation, giving the macro effect of multiple processes executing simultaneously, but at the micro level concurrency is not simultaneous, just splitting time into segments that allow multiple processes to execute rapidly and alternately.

  • Parallelism is two queues using two coffee machines at the same time
  • Concurrency is when two queues alternate using a coffeemaker

11.1.2 Concurrent Advantages of Go language

Some people compare Go to the C language of the 21st century, first, because the design of Go language is simple, second, the most important thing in the 21st century is parallel programming, and Go supports parallelism from the language level. At the same time, memory management for concurrent programs can be very complex, and Go provides an automatic garbage collection mechanism.

The built-in upper-layer apis for concurrent programming in Go are based on the CSP(Original sequential Processes) model. This means that explicit locking can be avoided because the Go language synchronizes data by sending and receiving data over photo-secure channels, greatly simplifying the writing of concurrent programs.

Normally, a typical desktop computer running a dozen or twenty threads would be overburdened, but this machine could easily have hundreds, thousands, or even thousands of Goroutines competing for resources.

11.2 goroutine

11.2.1 What is a goroutine

Goroutine is at the heart of Go’s concurrent design. A Goroutine is essentially a coroutine, but it’s smaller than a thread. A dozen goroutines may be as many as five or six threads at the bottom. Inside Go, you can share memory between these goroutines. Executing a goroutine requires very little stack memory (about 4 to 5KB), which of course scales accordingly. Because of this, thousands of concurrent tasks can be run simultaneously. Goroutine is easier to use, more efficient, and lighter than Thread.

11.2.2 create goroutine

Concurrent execution units can be created simply by adding the GO keyword to the front of a function call statement. The developer does not need to know any execution details, and the scheduler will automatically schedule it to execute on the appropriate system thread.

In concurrent programming, we usually want to take a process and break it up into pieces, and then let each Goroutine do its own piece of work. When a program starts, its main function runs in a separate goroutine, called main Goroutine. The new Goroutine is created using the GO statement.

Example code:

package main import ( "fmt" "time" ) func newTask() { i := 0 for { i++ fmt.Printf("new goroutine: I = %d\n", I) time.sleep (1 * time.second)}} func main() {// create a goroutine For {I ++ fmt.Printf("main goroutine: I = %d\n", I) time.sleep (1 * time.second)}}Copy the code

Program running result:

11.2.3 The main Goroutine exits first

When the main goroutine exits, all other work goroutine exits automatically:

func newTask() { i := 0 for { i++ fmt.Printf("new goroutine: I = %d\n", I) time.sleep (1 * time.second)}} func main() {// create a goroutine Go newTask() fmt.println ("main goroutine exit")}Copy the code

Program running result:

Reordering of read and write operations

Go may reorder the execution of some operations, which can guarantee that operations are executed sequentially in a single goroutine, but does not guarantee the execution order of multiple goroutines:

var _ = runtime.GOMAXPROCS(3) var a, B int func u1() {a = 1 b = 2} func u2() {a = 3 b = 4} func p() {println(a) println(b)} func main() {go u1() // multiple Go u2() go p() time.sleep (1 * time.second)}Copy the code

Operation effect:

If you want to keep multiple Goroutines executing in the same order as in your code, you can use the locking mechanism in a channel or sync package.

11.2.4 runtime package

11.2.4.1 GoschedThe goroutine is scheduled first

Runtime.gosched () is used to give up the CPU time slice, give up the execution rights of the current Goroutine, and the scheduler schedules other waiting tasks to run, resuming execution from that location at some point next time.

It’s like running A relay race. A runs for A while and hits the code Runtime.gosched () and passes the baton to B. A rests and B continues to run.

Example code:

// Create a goroutine go func(s string) {for I := 0; i < 2; i++ { fmt.Println(s) } }("world") for i := 0; i < 2; I++ {runtime.gosched () //import "runtime.gosched" /*  world world hello hello */ fmt.Println("hello") } }Copy the code

Priority scheduling

Your program might have one Goroutine that prevents other Goroutines from running at runtime, such as a for loop that doesn’t let the scheduler run:

func main() { done := false go func() { done = true }() for ! done { } println("done !" )}Copy the code

The body of the for loop does not have to be empty, but there is a problem if the code does not trigger scheduler execution.

The scheduler executes after GC, Go declarations, blocking channels, blocking system calls, and locking operations. It also executes on non-inline function calls:

func main() { done := false go func() { done = true }() for ! done { println("not done !" } println("done!") )}Copy the code

We can parse inline functions called in the for block by adding the -m argument:

You can also manually start the scheduler using Gosched() in the Runtime package:

func main() { done := false go func() { done = true }() for ! done { runtime.Gosched() } println("done !" )}Copy the code

Operation effect:

11.2.4.2 Goexit

Calling Runtime.goexit () immediately terminates the current goroutine execution, and the scheduler ensures that all registered defer calls are executed.

Example code:

Func main() {go func() {defer fmt.println (" a.efer ") func() {defer fmT.println (" b.efer ") runtime.goexit () // Terminate the current Goroutine, import "runtime" fmt.println ("B") // not execute}() fmt.println ("A") // Not execute}() For {}}Copy the code

Program running result:

11.2.4.3 GOMAXPROCS

Call Runtime.gomaxprocs () to set the maximum number of CPU cores that can be calculated in parallel and return the previous value.

Example code:

Func main () {/ / n: = runtime. GOMAXPROCS (1) / / print results: 111111111111111111110000000000000000000011111... N: = the runtime GOMAXPROCS (2) / / print results: 010101010101010101011001100101011010010100110... fmt.Printf("n = %d\n", n) for { go fmt.Print(0) fmt.Print(1) } }Copy the code

On the first execution (Runtime.gomaxProcs (1)), at most one goroutine can be executed at any one time. So we’re going to print a lot of ones.

After some time, the GO scheduler will put it to sleep and wake up another Goroutine, which will start printing lots of zeros. At the time of printing, the Goroutine is scheduled to the operating system thread.

On the second execution (Runtime.gomaxprocs (2)), we are using two cpus, so two goroutines can be executed together, alternately printing zeros and ones at the same frequency.

11.3 the channel

Goroutine runs in the same address space, so access to shared memory must be synchronized. Goroutine pursues shared memory by communication, not shared memory by communication.

The reference type channel is a concrete implementation of the CSP mode and is used for multiple Goroutine communications. Its internal implementation of synchronization to ensure concurrent security.

11.3.1 channel type

Like map, a channel is a reference to the underlying data structure created by make.

When we copy a channel or pass it as a function argument, we just copy a channel reference, so the caller and the called will refer to the same channel object. Channel’s zero value is nil, just like any other reference type.

When you define a channel, you also need to define the type of the value sent to the channel. A channel can be created using the built-in make() function:

Make (chan Type, 0) make(chan Type, capacity)Copy the code

When capacity= 0, the channel blocks reads and writes without buffering. When capacity> 0, the channel is buffered and non-blocking until the capacity is full.

A channel receives and sends data with the <- operator. Syntax for sending and receiving data:

Channel < -value // send value to channel <-channel // receive and discard it x := <-channel // receive data from channel and assign a value to x x, ok := <-channel // Also check whether the channel is closed or emptyCopy the code

By default, a channel is blocked until the other end is ready, making goroutine synchronization easier without the need for an explicit lock.

Example code:

Func main() {c := make(chan int) go func() {defer FMT.Println(" defer FMT.Println ") Println(" defer FMT.Println ") Println("num = ", num) FMT.Println(" end of main coroutine ")}() FMT.Println(" end of main coroutine ")}Copy the code

Program running result:

11.3.2 Unbuffered Channel

An unbuffered channel is one that does not have the ability to hold any value until it is received.

This type of channel requires both the sending and receiving Goroutine to be ready in order to complete the sending and receiving operations. If two Goroutines are not ready at the same time, the channel causes the goroutine that performed the send or receive operation first to block and wait.

This interaction of sending and receiving a channel is itself synchronous. Neither operation can exist in isolation from the other.

The following figure shows how two Goroutines share a value using unbuffered channels:

  • In step 1, both Goroutines arrive on the channel, but neither of them starts sending or receiving.
  • In step 2, the Goroutine on the left sticks its hand into the channel, which simulates the behavior of sending data to the channel. At this point, the Goroutine is locked in the channel until the swap is complete.
  • In step 3, the goroutine on the right puts its hand into the channel, which simulates receiving data from the channel. The goroutine will also be locked in the channel until the swap is complete.
  • In steps 4 and 5, the exchange is made, and finally, in step 6, both goroutines remove their hands from the channel, which simulates the release of the locked goroutine. Both Goroutines are now free to do other things.

Unbuffered channel creation format:

Make (chan Type, 0)Copy the code

If no buffer capacity is specified, the channel is synchronous and therefore blocks until the sender is ready to send and the receiver is ready to receive.

Example code:

Func main() {c := make(chan int, 0) // the built-in function len returns the number of buffered elements not read, Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c)) go func() {defer fmt.Println(" end of subcoroutine ") for I := 0; i < 3; I ++ {c < -i fmt.Printf(" subcoroutine is running [%d]: Len (c) = % d, cap (c) = % d \ n ", I, len (c), cap (c))}} () time. Sleep) / / (2 * time. The Second delay for 2 s I: = 0; i < 3; Println("num = ", num)} Println(" end of main coroutine ")}Copy the code

Program running result:

11.3.3 Buffered Channel

A buffered channel is a channel that stores one or more values before it is received.

This type of channel does not force goroutines to send and receive at the same time. The conditions under which a channel can block send and receive actions are also different. The receive action blocks only if there is no value to receive in the channel. The send action blocks only if the channel has no buffer available to hold the value being sent.

This leads to one big difference between buffered and unbuffered channels: the unbuffered channel guarantees that data will be exchanged between sending and receiving Goroutines at the same time; Buffered channels have no such guarantee.

The example diagram is as follows:

  • In step 1, the goroutine on the right is receiving a value from the channel.
  • In step 2, the goroutine on the right is independently receiving the value, while the goroutine on the left is sending a new value to the channel.
  • In step 3, the left Goroutine is still sending a new value to the channel, while the right Goroutine is receiving another value from the channel. The two operations in this step are neither synchronous nor block each other.
  • Finally, in step 4, all the sending and receiving is done, and there are a few more values in the channel, as well as some space for more.

Buffered channel creation format:

make(chan Type, capacity)
Copy the code

If a buffer capacity is given, the channel is asynchronous. As long as the buffer has unused space for sending data, or also contains data that can be received, its communication proceeds without blocking.

Example code:

Func main() {c := make(chan int, 3) // the built-in function len returns the number of buffered elements not read, Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c)) go func() {defer fmt.Println(" end of subcoroutine ") for I := 0; i < 3; I ++ {c < -i fmt.Printf(" subcoroutine is running [%d]: Len (c) = % d, cap (c) = % d \ n ", I, len (c), cap (c))}} () time. Sleep) / / (2 * time. The Second delay for 2 s I: = 0; i < 3; Println("num = ", num)} Println(" end of main coroutine ")}Copy the code

Program running result:

11.3.4 range and close

If the sender knows that there are no more values to send to the channel, then it is useful for the receiver to know in time that there are no additional values to receive, because the receiver can stop waiting unnecessarily to receive. This can be done using the built-in close function to close the channel.

Example code:

func main() { c := make(chan int) go func() { for i := 0; i < 5; If data, ok := < c; if data, ok := < c; Close (c)}() for {// if ok is true, channel is not closed, if false, channel is closed if data, ok := <-c; ok { fmt.Println(data) } else { break } } fmt.Println("Finished") }Copy the code

Program running result:

Note:

  • L channels don’t have to be closed as often as files, only when you really don’t have any data to send, or you want to explicitly end the range loop or something like that;
  • L After the channel is closed, data cannot be sent to the channel (a panic error is caused and zero value is returned immediately after receiving the channel);
  • L After the channel is closed, data can continue to be received from the channel.
  • L for nil channel, it’s going to block whether it’s sending or receiving.

You can use range to iterate over and over a channel:

func main() { c := make(chan int) go func() { for i := 0; i < 5; I ++ {c < -i} Close (c)}() for data := range c {fmt.println (data)} fmt.println ("Finished")} for data := range c {fmt.println (data)} fmT.println ("Finished")}Copy the code

11.3.5 Unidirectional Channel

By default, a channel is bidirectional, that is, it can send data to or receive data from it.

However, we often see a channel being passed as a parameter and a value that wants to be used in one direction, either to send data only or to receive data only, in which case we can specify the direction of the channel.

The declaration of the one-way channel variable is very simple, as follows:

Var ch2 chan< -float64 // ch2 is a uni-direction channel. Var ch3 <-chan int // Ch3 is a unidirectional channel and is used to read only int dataCopy the code

L chan<- indicates that the data is going into the pipe. To write the data to the pipe, it is output to the caller.

L <-chan means that the data comes out of the pipe, and for the caller it is the data from the pipe, which is of course the input.

You can implicitly convert a channel to a one-way queue, receiving or sending only, but you cannot convert a one-way channel to a normal channel:

c := make(chan int, 3) var send chan<- int = c // send-only var recv <-chan int = c // receive-only send <- 1 //<-send //invalid operation: <-send (receive from send-only type chan<- int) <-recv //recv <- 2 //invalid operation: Recv < -2 (send to receive-only type <-chan int) // A one-way channel cannot be converted to a common channel. D1 := (chan int)(send) // Cannot convert send (type chan<- int) to type chan int d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan intCopy the code

Example code:

Func counter(out chan< -int) {defer close(out) for I := 0; i < 5; {for num := range in {fmt.println (num)}} // <-chan // func printer(in <-chan int) {for num := range in {fmt.println (num)}} Func main() {c := make(chan int) // chan // go counter(c) // printer(c) // consumer FMT.Println("done")}Copy the code

11.3.6 timer

11.3.6.1 Timer

A Timer is a Timer that represents a single event in the future, and you can tell the Timer how long you have to wait, and it provides a channel, and at that time in the future that channel provides a time value.

Example code:

Import "FMT" import "time" func main() { Timer1 := time.newtimer (time.second * 2) t1: = time.now () // the current time FMT.Printf("t1: %v\n", t1) t2 := <-timer1.C fmt.Printf("t2: %v\n", t2) // If you just want to wait, Timer2 := time.newtimer (time.second * 2) < -timer2.c FMT.Println(" after 2s ") time.sleep (time.second * 2) FMT.Println(" After(time.second * 2) FMT.Println(" After(time.second) ") timer3 := time.newtimer (time.second) go Func () {< -timer3.c fmt.println ("Timer 3 expired")}() stop := timer3.stop () if stop {fmt.println ("Timer 3 ") Println("before") timer4 := time.newtimer (time.second * 5) // timer4.reset (time.second * 1) // Reset time < -timer4.c fmt.println ("after")}Copy the code

11.3.6.2 Ticker

The Ticker is a timed timer that sends an event (the current time) to a channel at an interval, and the receiver of the channel can read events from the channel at a fixed interval.

Example code:

Func main() {// Create a timer. After 1 second, Ticker := time.newTicker (time.second * 1) I := 0 go func() {for {// loop < -ticker.ci ++ Println(" I = ", I) if I == 5 {ticker.stop () // Stop the timer}}}()Copy the code

11.4 select

11.4.1 the select action

Go provides a keyword to listen for data flows on a channel.

The use of SELECT is very similar to the Switch language. Select starts a new selection block, and each selection condition is described by a case statement.

In contrast to the switch statement, the select statement has many restrictions. The biggest restriction is that each case statement must be an IO operation. The structure is as follows:

Select {case <-chan1: // case chan2 < -1: // case chan2 < -1: // case chan2 < -1: // case chan2 < -1: // // If none of the above is successful, enter the default process}Copy the code

In a SELECT statement, the Go language evaluates each sent and received statement from start to finish, in order.

If any of the statements can continue to execute (that is, they are not blocked), then use any of the statements that can be executed.

If none of the statements can be executed (that is, all channels are blocked), there are two possible situations:

  • If the default statement is given, the default statement is executed and program execution resumes from the statement following the SELECT statement.
  • If there is no default statement, then the SELECT statement will be blocked until at least one communication can proceed.

Example code:

func fibonacci(c, quit chan int) {
    x, y := 1, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
 
func main() {
    c := make(chan int)
    quit := make(chan int)
 
    go func() {
        for i := 0; i < 6; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
 
    fibonacci(c, quit)
}
Copy the code

The running results are as follows:

11.4.2 timeout

Sometimes the goroutine will block, so how can we prevent the entire program from blocking? We can use select to set the timeout as follows:

func main() { c := make(chan int) o := make(chan bool) go func() { for { select { case v := <-c: fmt.Println(v) case <-time.After(5 * time.Second): Println("timeout") o < -true break}}() //c < -666 //Copy the code