Analysis of channel in Golang

Channel is a communication method between goroutines provided by Golang at the language level. Channels can be used to pass messages between two or more goroutines. Channel is an intra-process communication method, so the process of passing objects through channels is consistent with the behavior of parameter passing when calling functions, for example, pointers can also be passed. Using channels to send and receive the required shared resource eliminates race conditions between goroutines.

When a resource needs to be shared between goroutines, a channel bridges the gap between goroutines and provides a mechanism to ensure that data is exchanged synchronously. Channel is type-dependent, that is, a channel can only pass one type of value, and this type needs to be specified when declaring the channel. Values ​​or pointers of built-in types, named types, structure types, and reference types can be shared through channels.

basic grammar

The syntax for declaring a channel is:

var ChannelName chan ElementType

The difference from the general variable declaration is only that a chan keyword is added in front of the type. ElementType indicates the type of data that this channel can transfer. For example, declare a channel passing int type:

var ch chan int

Or declare a map whose elements are channels of bool type:

var m map[string] chan bool

In Golang, you need to use the built-in make function class to create an instance of channel:

ch := make(chan int)

This declares and initializes a channel of type int named ch. The syntax for sending and receiving data using a channel is also very intuitive. For example, the following code sends data to a channel:

ch <- value

Writing data to a channel usually causes the program to block until another goroutine reads data from the channel. The following code reads data from a channel into a variable:

value := <-ch

Note that if there is no data in the channel, reading data from the channel will also cause the program to block until data is written to the channel.

According to whether the channel has a buffer or not, the channel can be simply divided into a channel without a buffer and a channel with a buffer. The usage of these two types of channels will be introduced in detail in the following sections of this article.

select

The select function in the Linux system is used to monitor a series of file handles. Once an I/O action occurs on one of the file handles, the select function will return. This function is mainly used to implement high concurrent socket server programs. The select keyword in Golang is somewhat similar to the select function in linux, and it is mainly used to deal with asynchronous I/O problems.

The syntax of select is very similar to that of switch. Select starts a new selection block, and each selection condition is described by a case statement. Compared with the switch statement that can select any condition that can use equality comparison, select has more restrictions, the biggest one of which is that each case statement must be an I/O operation. Its general structure is as follows:

select {
    case <-chan1:       // 如果 chan1 成功读取到数据,则执行该 case 语句
    case chan2 <- 1:    // 如果成功向 chan2 写入数据,则执行该 case 语句
    default:            // 如果上面的条件都没有成功,则执行 default 流程
}

It can be seen that select is not like switch, and there is no conditional judgment behind it, but directly check the case statement. Each case statement must be a channel-oriented operation. For example, in the above example, the first case tries to read a data from chan1 and ignore the read data directly, while the second case tries to write an integer 1 to chan2, if neither of them succeeds, Then execute the default statement.

unbuffered channel

An unbuffered channel (unbuffered channel) is a channel that has no ability to hold any value before receiving it. This type of channel requires the sending goroutine and the receiving goroutine to be ready at the same time to complete the sending and receiving operations. If both goroutines are not ready at the same time, the channel will cause the goroutine that performed the send or receive operation first to block waiting. This interaction of sending and receiving to a channel is inherently synchronous. Neither of these operations can exist without the other. We can visualize how two goroutines use unbuffered channels to share a value through the following diagram (the diagram below is from the Internet):

Let's explain the above figure in detail:

  • In step 1, both goroutines arrive at the channel, but neither starts sending or receiving data.
  • In step 2, the goroutine on the left puts its hand into the channel, which simulates the act of sending data to the channel. At this point, the goroutine will be locked in the channel until the exchange is complete.
  • In step 3, the right goroutine puts its hand into the channel, which simulates receiving data from the channel. This goroutine will also be locked in the channel until the exchange is complete.
  • In steps 4 and 5, data exchange is performed.
  • In step 6, both goroutines take their hands off the channel, which simulates the release of the locked goroutine. Both goroutines are now free to do other things.

The following example simulates a tennis match. In tennis, two players pass the ball back and forth between the two. Players are always in one of two states: either waiting to catch the ball, or hitting the ball to the opponent. A tennis game can be simulated using two goroutines, and an unbuffered channel to simulate the ball going back and forth:

// 这个示例程序展示如何用无缓冲的通道来模拟
//2个goroutine间的网球比赛
package main

import(
    "math/rand"
    "sync"
    "time"
    "fmt"
)

// wg用来等待程序结束
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// main是所有Go程序的入口
func main() {
    // 创建一个无缓冲的通道
    court := make(chan int)

    // 计数加2,表示要等待两个goroutine
    wg.Add(2)

    // 启动两个选手
    go player("Nick", court)
    go player("Jack", court)

    // 发球
    court <- 1

    // 等待游戏结束
    wg.Wait()
}

// player 模拟一个选手在打网球
func player(name string, court chan int) {
    // 在函数退出时调用Done来通知main函数工作已经完成
    defer wg.Done()

    for{
        // 等待球被击打过来
        ball, ok := <-court
        if !ok {
            // 如果通道被关闭,我们就赢了
            fmt.Printf("Player %s Won\n", name)
            return
        }

        // 选随机数,然后用这个数来判断我们是否丢球
        n := rand.Intn(100)
        if n%5 == 0 {
            fmt.Printf("Player %s Missed\n", name)

            // 关闭通道,表示我们输了
            close(court)
            return
        }

        // 显示击球数,并将击球数加1
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++

        // 将球打向对手
        court <- ball
    }
}

Running the above code will output information similar to the following:

Player Jack Hit 1
Player Nick Hit 2
Player Jack Hit 3
Player Nick Hit 4
Player Jack Missed
Player Nick Won

Briefly explain the above code:
an unbuffered channel of int type is created in the main function, and the channel is used to allow two goroutines to synchronize with each other when hitting the ball. Then two goroutines that participate in the race are created. At this point, both goroutines are blocked waiting to hit the ball. court <- 1 Simulate serving, send the ball into the channel, and the program starts to execute the game until a goroutine loses the game.
In the player function, the main thing is to run an infinite loop for statement. In this loop is the process of playing games. The goroutine receives data from the channel, which is used to indicate that it is waiting to receive the ball. This receive action will lock the goroutine until data is sent to the channel. When the receiving action of the channel returns, it will check whether the ok flag is false. If the value is false, the channel has been closed and the game is over. In this simulation, random numbers are used to decide whether or not a goroutine hits the ball. If the ball is hit, the value of ball is incremented by 1 and the ball is put back into the channel as a ball to be sent to another player. At this point, both goroutines are locked until the swap is complete. Eventually, causing a goroutine to miss the ball will close the channel. After that, both goroutines will return, the Done declared by defer will be executed, and the program will terminate.

buffered channel

A buffered channel (buffered channel) is a channel that can store one or more values ​​before being received. This type of channel does not enforce that sending and receiving between goroutines must be done at the same time. The conditions under which a channel will block send and receive actions are also different. The receive action blocks only if there are no values ​​to receive in the channel. The send action blocks only if the channel has no available buffers to hold the value being sent. This leads to a big difference between buffered and unbuffered channels: unbuffered channels guarantee that the sending and receiving goroutines will exchange data at the same time; buffered channels have no such guarantee. You can visually understand that two goroutines add a value to the buffered channel and remove a value from the buffered channel through the following diagram (the following figure is from the Internet):

Let's explain the above figure in detail:

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

Creating a channel with a buffer is very simple, just add another buffer size, for example, create a channel that transfers int type data with a buffer of 10:

ch := make(chan int, 10)

The demo below uses a set of goroutines to receive and complete tasks, and buffered channels provide a clear and intuitive way to do this:

// 这个示例程序展示如何使用
// 有缓冲的通道和固定数目的
// goroutine来处理一堆工作
package main

import(
    "math/rand"
    "sync"
    "time"
    "fmt"
)

const(
    numberGoroutines = 2 // 要使用的goroutine的数量
    taskLoad = 5 // 要处理的工作的数量
)

// wg用来等待程序结束
var wg sync.WaitGroup

func init()  {
    rand.Seed(time.Now().UnixNano())
}

// main是所有Go程序的入口
func main()  {
    // 创建一个有缓冲的通道来管理工作
    tasks := make(chan string, taskLoad)
    
    // 启动goroutine来处理工作
    wg.Add(numberGoroutines)
    for gr := 1; gr <= numberGoroutines; gr++ {
        go worker(tasks, gr)
    }
    
    // 增加一组要完成的工作
    for post := 1; post <= taskLoad; post++ {
        tasks <- fmt.Sprintf("Task: %d", post)
    }
    
    // 当所有工作都处理完时关闭通道
    // 以便所有goroutine退出
    close(tasks)
    
    // 等待所有工作完成
    wg.Wait()
}

// worker作为goroutine启动来处理
// 从有缓冲的通道传入的工作
func worker(tasks chan string, worker int) {
    // 通知函数已经返回
    defer wg.Done()

    for{
        // 等待分配工作
        task, ok := <-tasks
        if !ok{
            // 这意味着通道已经空了,并且已被关闭
            fmt.Printf("Worker: %d: Shutting Down\n", worker)
            return
        }

        // 显示我们开始工作了
        fmt.Printf("Worker: %d: Started %s\n", worker, task)

        // 随机等一段时间来模拟工作
        sleep := rand.Int63n(100)
        time.Sleep(time.Duration(sleep)* time.Millisecond)

        // 显示我们完成了工作
        fmt.Printf("Worker: %d: Completed %s\n", worker, task)
    }
}
Running the above program, the output is roughly as follows:
Worker: 2: Started Task: 1
Worker: 1: Started Task: 2
Worker: 1: Completed Task: 2
Worker: 1: Started Task: 3
Worker: 1: Completed Task: 3
Worker: 1: Started Task: 4
Worker: 2: Completed Task: 1
Worker: 2: Started Task: 5
Worker: 1: Completed Task: 4
Worker: 1: Shutting Down
Worker: 2: Completed Task: 5
Worker: 2: Shutting Down

There are very detailed comments in the code, so I won't repeat it, just explain the closing of the channel:
the code for closing the channel is very important. When the channel is closed, goroutine can still receive data from the channel, but can no longer send data to the channel. It is important to be able to receive data from a channel that has been closed, as this allows the channel to be closed with all values ​​buffered in it fetched without data loss. Getting data from a channel that has been closed and has no data will always return immediately, and return a zero value of the channel type. If the optional flag is also added when getting the channel, the status information of the channel can be obtained.

processing timeout

Care should be taken when using channels, for example for the following simple usage:

i := <-ch

If no data is ever written to ch, then this read action will never be able to read data from ch, and the result is that the entire goroutine is blocked forever and there is no chance of recovery. If the channel is only used by the same developer, then the possibility of problems is lower. But once it is made public, worst-case scenarios must be considered and the program maintained.

Golang does not provide a direct timeout processing mechanism, but it can be solved flexibly by using the select mechanism. Because the feature of select is that as long as one of the cases has been completed, the program will continue to execute without considering other cases. Based on this feature, let's implement a channel timeout mechanism:

ch := make(chan int)
// 首先实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
    time.Sleep(1e9) // 等待 1 秒
    timeout <- true
}()
// 然后把 timeout 这个 channel 利用起来
select {
case <-ch:
    // 从 ch 中读取到数据
case <- timeout:
    // 一直没有从 ch 中读取到数据,但从 timeout 中读取到了数据
    fmt.Println("Timeout occurred.")
}

Execute the above code, the output result is:

Timeout occurred.

close channel

Closing the channel is very simple, just call the built-in close() function of Golang directly:

close(ch)

After closing the channel, the problem we have to face is: how to judge whether a channel is closed?
In fact, while reading data from the channel, you can also get a Boolean value, which indicates whether the channel is closed:

x, ok := <-ch

If the value of ok is false, it means that ch has been closed.

Guess you like

Origin blog.csdn.net/qq_39312146/article/details/130632224