Home  >  Article  >  Backend Development  >  Detailed introduction to channels in go language

Detailed introduction to channels in go language

尚
forward
2019-11-25 16:29:513970browse

Detailed introduction to channels in go language

This article is about channel in go language. The reason why I write it is because I feel that channel is very important. At the same time, channel is also an important support point for go concurrency. Because go uses message passing shared memory instead of using shared memory to communicate.

Concurrent programming is very good, but concurrency is very complicated. The difficulty lies in coordination. How to handle the communication between various programs is very important. Before writing about the usage and characteristics of channels, we need to review inter-process communication in the operating system.

Inter-process communication

There are two general communication models in engineering: shared data and messages. As the name suggests, process communication refers to the exchange of information between processes, because mutual exclusion and synchronization of processes require the exchange of information between processes. Anyone who has studied operating systems knows that process communication can be roughly divided into low-level process communication and high-level process communication. Now it is basically All above are advanced process communications. Among them, advanced communication mechanisms can be divided into: message passing system, shared memory system, pipeline communication system and client server system.

1. Message passing system

It does not rely on any shared storage area or a certain data structure. It uses formatted messages as units to use the system to provide Communication primitives are used to complete data exchange, which seems to be very inefficient.
2. Shared memory system

Communicating processes share storage areas or data structures. Processes communicate through these spaces. This method is relatively common, such as a certain file as a carrier.

3. Client server system

Several other communication mechanisms are basically on the same computer (it can be said to be the same environment). Of course, in some cases Cross-computer communication can be achieved below. The client-server system is different. My understanding is that it can be regarded as an IP request, and a client requests to connect to a server.

This method is more popular on the Internet now. Remote scheduling is now more commonly used, such as RPC (which sounds very high-end, but in fact it has been available in the operating system for a long time) and sockets. , socket, this is quite commonly used and is closely related to our programming, because you will find that many services need to use RPC calls.

4. Pipeline Communication Department

Finally, let’s talk about the mechanism of pipe communication in detail. At the operating system level, pipes are used to link a reading process and a writing process. files that enable communication between them. It is called a pipe file on the system.

The implemented mechanism is such as: the pipeline provides the following two functions

1. Mutual exclusivity. When a process is performing a read or write operation on a pipe file, other processes must Wait or block or sleep.

2. Synchronicity. When the writing (input) process writes the pipe file, it will wait or block or sleep until the reading (output) process wakes it up after taking the data. Similarly, when the reading process goes When reading an empty pipe file, it will also wait, block, or sleep until the writing process wakes it up after writing to the pipe.

Use of channel

The channel corresponding to go should be the fourth type. The channel of go language is a communication method between goroutines provided at the language level. It makes no sense to talk about channel alone, because it is effective together with goroutine. Let's first look at how general languages ​​solve shared memory between programs.

The following is a program we are familiar with:

package main

import "fmt"

var counts int = 0

func Count() {
    counts++
    fmt.Println(counts)
}
func main() {

    for i := 0; i < 3; i++ {
        go Count()
    }
}

Anyone who has learned go should know the reason, because: the Go program initializes the main() method and package, and then executes the main() function , but when the main() function returns, the program will exit. The main program does not wait for other goroutines, resulting in no output.

Let’s take a look at how conventional languages ​​solve this concurrency problem:

package main

import "fmt"
import "sync"
import "runtime"

var counts int = 0

func Count(lock *sync.Mutex) {
    lock.Lock()
    counts++
    fmt.Println(counts)
    lock.Unlock()
}
func main() {
    lock := &sync.Mutex{}

    for i := 0; i < 3; i++ {
        go Count(lock)
    }

    for {
        lock.Lock()
        c := counts
        lock.Unlock()

        runtime.Gosched()

        if c >= 3 {
            break
        }

    }
}

The solution is a bit funny, adding a bunch of locks, because its execution is like this: code For the lock variable in, every operation on counts must be locked first. After the operation is completed, the lock must be unlocked. In the main function, a for loop is used to continuously check the value of counter, and of course the lock must also be locked.

When its value reaches 3, it means that all goroutines have been executed. At this time, the main function returns and the program exits. This method is the preferred way to solve concurrency in popular languages. You can see that a lot of things have been written to solve concurrency. If a project is beginning to take shape, I don’t know how many locks need to be added.

Let’s see how channel solves this problem:

package main

import "fmt"

var counts int = 0

func Count(i int, ch chan int) {
    fmt.Println(i, "WriteStart")
    ch <- 1
    fmt.Println(i, "WriteEnd")
    fmt.Println(i, "end", "and echo", i)
    counts++
}

func main() {
    chs := make([]chan int, 3)
    for i := 0; i < 3; i++ {
        chs[i] = make(chan int)
        fmt.Println(i, "ForStart")
        go Count(i, chs[i])
        fmt.Println(i, "ForEnd")
    }

    fmt.Println("Start debug")
    for num, ch := range chs {
        fmt.Println(num, "ReadStart")
        <-ch
        fmt.Println(num, "ReadEnd")
    }

    fmt.Println("End")

    //为了使每一步数值全部打印
    for {
        if counts == 3 {
            break
        }
    }
}

In order to see clearly the steps of goroutine execution and the characteristics of channel, I specially printed each step. The following is the execution Interested students can try it by themselves. The printing order may be different:

Detailed introduction to channels in go language

Let’s analyze this process and see the role of channel in it. The main program starts:

Print "0 ForStart 0 ForEnd", indicating that i = 0 this cycle has started execution, and the first goroutine has started;

打印 "1 ForStart"、"1 ForEnd"、"2 ForStart"、"2 ForEnd" 说明3次循环都开始,现在系统中存在3个goroutine;

打印 "Start debug",说明主程序继续往下走了,

打印 "0 ReadStar"t ,说明主程序执行到for循环,开始遍历chs,一开始遍历第一个,但是因为此时 i = 0 的channel为空,所以该channel的Read操作阻塞;

打印 "2 WriteStart",说明第一个 i = 2 的goroutine先执行到Count方法,准备写入channel,因为主程序读取 i = 0 的channel的操作再阻塞中,所以 i = 2的channel的读取操作没有执行,现在i = 2 的goroutine 写入channel后下面的操作阻塞;

打印 "0 WriteEnd",说明 i = 0 的goroutine也执行到Count方法,准备写入channel,此时主程序 i = 0 的channel的读取操作被唤醒;

打印 "0 WriteEnd" 和 "0 end and echo 0" 说明写入成功;

打印 "0 ReadEnd",说明唤醒的 i = 0 的channel的读取操作已经唤醒,并且读取了这个channel的数据;

打印 "0 ReadEnd",说明这个读取操作结束;

打印 "1 ReadStart",说明 i = 1 的channel读取操作开始,因为i = 1 的channel没有内容,这个读取操作只能阻塞;

打印 "1 WriteStart",说明 i = 1 的goroutine 执行到Count方法,开始写入channel 此时 i = 1的channel读取操作被唤醒;

打印 "1 WriteEnd" 和 "1 end and echo 1" 说明 i = 1 的channel写入操作完成;

打印 "1 ReadEnd",说明 i = 1 的读取操作完成;

打印 "2 ReadStart",说明 i = 2 的channel的读取操作开始,因为之前已经执行到 i = 2 的goroutine写入channel操作,只是阻塞了,现在因为读取操作的进行,i = 2的写入操作流程继续执行;

打印 "2 ReadEnd",说明 i = 2 的channel读取操作完成;

打印 "End" 说明主程序结束。

此时可能你会有疑问,i = 2 的goroutine还没有结束,主程序为啥就结束了,这正好印证了我们开始的时候说的,主程序是不等待非主程序完成的,所以按照正常的流程我们看不到 i = 2 的goroutine的的完全结束,这里为了看到他的结束我特意加了一个 counts 计算器,只有等到计算器等于3的时候才结束主程序,接着就出现了打印 "2 WriteEnd" 和 "2 end and echo 2"  到此所有的程序结束,这就是goroutine在channel作用下的执行流程。

上面分析写的的比较详细,耐心看两遍基本上就明白了,主要帮助大家理解channel的写入阻塞和读入阻塞的应用。

基本语法

channel的基本语法比较简单, 一般的声明格式是:

var ch chan ElementType

定义格式如下:

ch := make(chan int)

还有一个最常用的就是写入和读出,当你向channel写入数据时会导致程序阻塞,直到有其他goroutine从这个channel中读取数据,同理如果channel之前没有写入过数据,那么从channel中读取数据也会导致程序阻塞,直到这个channel中被写入了数据为止

ch <- value    //写入
value := <-ch  //读取

关闭channel

close(ch)

判断channel是否关闭(利用多返回值的方式):

 b, status := <-ch

带缓冲的channel,说起来也容易,之前我们使用的都是不带缓冲的channel,这种方法适用于单个数据的情况,对于大量的数据不太实用,在调用make()的时候将缓冲区大小作为第二个参数传入就可以创建缓冲的channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

c := make(chan int, 1024)

单项channel,单向channel只能用于写入或者读取数据。channel本身必然是同时支持读写的,否则根本没法用。所谓的单向channel概念,其实只是对channel的一种使用限制。单向channel变量的声明:

var ch1 chan int   // ch1是一个正常的channel
var ch2 <-chan int // ch2是单向channel,只用于读取int数据

单项channel的初始化

ch3 := make(chan int)
ch4 := <-chan int(ch3) // ch4是一个单向的读取channel

超时机制

超时机制其实也是channel的错误处理,channel固然好用,但是有时难免会出现实用错误,当是读取channel的时候发现channel为空,如果没有错误处理,像这种情况就会使整个goroutine锁死了,无法运行。

我找了好多资料和说法,channel 并没有处理超时的方法,但是可以利用其它方法间接的处理这个问题,可以使用select机制处理,select的特点比较明显,只要有一个case完成了程序就会往下运行,利用这种方法,可以实现channel的超时处理:

原理如下:我们可以先定义一个channel,在一个方法中对这个channel进行写入操作,但是这个写入操作比较特殊,比如我们控制5s之后写入到这个channel中,这5s时间就是其他channel的超时时间,这样的话5s以后如果还有channel在执行,可以判断为超时,这是channel写入了内容,select检测到有内容就会执行这个case,然后程序就会顺利往下走了。

实现如下:

timeout := make(chan bool, 1)
go func() {
    time.Sleep(5s) // 等待s秒钟
    timeout <- true
}()

select {
    case <-ch:
    // 从ch中读取到数据
    case <-timeout:
    // 没有从ch中读取到数据,但从timeout中读取到了数据
}

推荐:go语言教程

The above is the detailed content of Detailed introduction to channels in go language. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:cnblogs.com. If there is any infringement, please contact admin@php.cn delete