[Go] Channel of concurrent programming

Posted by gordonmc on Mon, 21 Feb 2022 04:19:00 +0100

1, What is channel for? For communication between goroutine s

Simply executing functions concurrently is meaningless. Functions need to exchange data between functions to reflect the significance of concurrent execution of functions.

Although shared memory can be used for data exchange, shared memory is prone to race problems in different goroutine s. In order to ensure the correctness of data exchange, the memory must be locked with mutex, which is bound to cause performance problems.

The concurrency model of Go language is CSP (Communicating Sequential Processes), which advocates sharing memory through communication, rather than realizing communication through sharing memory. This is in the previous section [Go] concurrent programming Also mentioned in.

If goroutine is the concurrent executor of Go program, channel is the connection between them. Channel is a communication mechanism that allows one goroutine to send a specific value to another goroutine.

Channel in Go language is a special type. The channel is like a conveyor belt or queue. It always follows the First In First Out rule to ensure the order of sending and receiving data. Each channel is a conduit of a specific type, that is, when declaring a channel, you need to specify the element type to be transmitted.

2, channel is a data type

Channel is a type, a reference type. The format of declaring channel type is as follows: (use chan keyword)

var Variable name chan Element type

The variable name is the channel variable name, and the element type is the type of data transmitted by the channel.

For example:

var ch1 chan int   // Declare a channel ch1 that passes an integer 
var ch2 chan bool  // Declare a pass Boolean channel ch2 
var ch3 chan []int // Declare a channel ch3 that passes int slices 

3, Create channel

1. Declare channel

The channel is a reference type, and the null value of the channel type is nil.

package main

import (
	"fmt"
)

func main() {

	var ch chan int
	fmt.Println(ch) // <nil>

}

Output result:

<nil>

If only the channel is declared, the channel cannot be used.

2. Generally, make is used to complete declaration + creation in one step

The channel needs to be initialized with the make function before it can be used.

The format of creating a channel using the make function is as follows:

make(chan Element type, [Buffer size])

The buffer size of channel is optional.

Use example of make function:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

example:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	fmt.Println(ch)
}

Output result:

0xc00004a060

4, channel operation

The channel has three operations: send, receive and close.

Both sending and receiving use the < - symbol.

Now let's create a channel with the following statement:

ch := make(chan int)

send out

Sends a value to the channel.

ch <- 10 // Send 10 to ch

receive

Receive values from a channel.

x := <- ch // Receives the value from ch and assigns it to the variable x
<-ch       // Receive value from ch, ignore result

close

We close the channel by calling the built-in close function.

close(ch)

Note that the shutdown here is not complete, but the channel is closed for the sender. That is, you can no longer send values to the channel, but you can still receive residual values from it.

The thing to note about closing the channel is that the channel needs to be closed only when the receiver is notified that all data of goroutine has been sent.

The channel can be recycled by the garbage collection mechanism. It is different from closing the file. Closing the file after the operation is necessary, but closing the channel is not necessary.

The closed channel has the following characteristics:

  1. Sending a value to a closed channel will result in panic.
  2. Receiving a closed channel will get the value until the channel is empty.
  3. Performing a receive operation on a closed channel with no value will get the corresponding type of zero value.
  4. Closing a closed channel will cause panic.

5, Unbuffered channel

Unbuffered channels are also called blocked channels. Let's look at the following code:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	ch <- 10   //Send a value of 10 to the channel
	fmt.Println("Sent successfully")
}

Output result:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	D:/liteide/mysource/src/hello/main.go:9 +0x37

It can be seen that although it passed the compilation, it reported an error. Why is there a deadlock error?

Because we use ch: = make (Chan int) to create an unbuffered channel. Unbuffered channels can send values only when someone receives them. Just as there is no express cabinet and collection point in your community, the courier must send this item to your hand when calling you. In short, the unbuffered channel must be received before sending.

The above code will block the code in the line ch < - 10 to form a deadlock. How to solve this problem?

One method is to enable a goroutine to receive values, for example:

package main

import (
	"fmt"
)

func recv(c chan int) { //Function that receives values from the channel
	ret := <-c
	fmt.Println("Received successfully", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // Enable goroutine to receive values from the channel
	ch <- 10
	fmt.Println("Sent successfully")
}

Output result:

Received successfully 10
 Sent successfully

The sending operation on the unbuffered channel will be blocked until another goroutine performs the receiving operation on the channel. At this time, the value can be sent successfully, and the two goroutines will continue to execute. Conversely, if the receive operation is performed first, the receiver's goroutine will block until another goroutine sends a value on the channel.

The use of unbuffered channels for communication will result in the synchronization of the goroutine sent and received. Therefore, the unbuffered channel is also called the synchronous channel.

A bufferless channel is equivalent to a buffered channel with a channel capacity of 0.

6, Buffered channel

Another way to solve the above problem is to use channels with buffers.

We can specify the capacity of the channel when initializing the channel with the make function, for example:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 1) // Create a buffered channel with a capacity of 1
	ch <- 10
	fmt.Println("Sent successfully")
}

Output result:

Sent successfully

As long as the capacity of the channel is greater than zero, the channel is a buffered channel. The capacity of the channel indicates the number of elements that can be stored in the channel. Just like the express cabinet in your community has only so many grids. When the grid is full, it will not fit, and it will be blocked. When someone else takes one, the courier can put one in it.

We can use the built-in len function to obtain the number of existing elements in the channel and the cap function to obtain the capacity of the channel, although we rarely do so.

7, close()

You can close the channel through the built-in close() function (remember to close the pipeline if you no longer save values or take values into the channel)

package main

import "fmt"

func main() {
	c := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			c <- i
		}
		close(c)  //Close the channel after setting the value
	}()
	for {
		if data, ok := <-c; ok {
			fmt.Println(data)
		} else {
			break
		}
	}
	fmt.Println("main end")
}

Output result:

0
1
2
3
4
main end

8, How to get values from the channel loop gracefully (is the channel closed?)

When sending limited data through the channel, we can close the channel through the close() function to tell the goroutine receiving the value from the channel to stop waiting. When the channel is closed, sending a value to the channel will trigger panic, and the value received from the channel is always of type zero. How to judge whether a channel is closed?

Let's look at the following example:

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	
	// Turn on goroutine to send the number from 0 to 99 to ch1
	go func() {
		for i := 0; i < 100; i++ {
			ch1 <- i
		}
		close(ch1)   //Close ch1 after sending
	}()
	
	// Turn on goroutine to receive the value from ch1 and send the square of the value to ch2
	go func() {
		for {
			i, ok := <-ch1  // After the channel is closed, the value is ok=false
			if !ok {
				break
			}
			ch2 <- i * i
		}
		close(ch2)
	}()
	
	// Receive print value from ch2 in main goroutine
	for i := range ch2 { // After the ch2 channel is closed, it will exit the for range cycle
		fmt.Println(i)
	}
}

Output result:

0
1
4
9
16
25
36
49
64
81
100
121
...
9409
9604
9801

In the above example, we can judge whether the channel is closed in two ways:

1. i, ok := <-ch1
2. for i := range ch2

We usually use for range.

9, Unidirectional channel

Sometimes we will pass the channel as a parameter between multiple task functions. Many times, when we use the channel in different task functions, we will restrict it. For example, we can only send or receive the channel in the function.

The Go language provides a one-way channel to deal with this situation. For example, we transform the above example as follows:

package main

import "fmt"

func counter(out chan<- int) { //insurance against total loss only
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) { //Receive only the square from in and then send it to out
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {  //Receive only
	for i := range in {
		fmt.Println(i)
	}
}

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go counter(ch1)
	go squarer(ch2, ch1)
	printer(ch2)
}

Output result:

0
1
4
9
16
25
36
49
...
9409
9604
9801

Among them,

1. chan<- int It is a channel that can only be sent, which can be sent but cannot be received;
2. <-chan int It is a channel that can only receive, but can not send.

It is possible to convert a two-way channel into a one-way channel in the function transfer parameter and any assignment operation, but the reverse is not possible.

10, Channel summary

Common channel exceptions are summarized as follows:

be careful:
Closing a closed channel will also cause panic.

Reference link

  1. Channel

Topics: Go Back-end