learn-tech/专栏/Go语言核心36讲/10通道的基本操作.md
2024-10-16 00:01:16 +08:00

179 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
10 通道的基本操作
作为Go语言最有特色的数据类型通道channel完全可以与goroutine也可称为go程并驾齐驱共同代表Go语言独有的并发编程模式和编程哲学。
Dont communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
这是作为Go语言的主要创造者之一的Rob Pike的至理名言这也充分体现了Go语言最重要的编程理念。而通道类型恰恰是后半句话的完美实现我们可以利用通道在多个goroutine之间传递数据。
前导内容:通道的基础知识
通道类型的值本身就是并发安全的这也是Go语言自带的、唯一一个可以满足并发安全性的类型。它使用起来十分简单并不会徒增我们的心智负担。
在声明并初始化一个通道的时候我们需要用到Go语言的内建函数make。就像用make初始化切片那样我们传给这个函数的第一个参数应该是代表了通道的具体类型的类型字面量。
在声明一个通道类型变量的时候,我们首先要确定该通道类型的元素类型,这决定了我们可以通过这个通道传递什么类型的数据。
比如类型字面量chan int其中的chan是表示通道类型的关键字而int则说明了该通道类型的元素类型。又比如chan string代表了一个元素类型为string的通道类型。
在初始化通道的时候make函数除了必须接收这样的类型字面量作为参数还可以接收一个int类型的参数。
后者是可选的用于表示该通道的容量。所谓通道的容量就是指通道最多可以缓存多少个元素值。由此虽然这个参数是int类型的但是它是不能小于0的。
当容量为0时我们可以称通道为非缓冲通道也就是不带缓冲的通道。而当容量大于0时我们可以称为缓冲通道也就是带有缓冲的通道。非缓冲通道和缓冲通道有着不同的数据传递方式这个我在后面会讲到。
一个通道相当于一个先进先出FIFO的队列。也就是说通道中的各个元素值都是严格地按照发送的顺序排列的先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-我们也可以叫它接送操作符一个左尖括号紧接着一个减号形象地代表了元素值的传输方向
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",
elem1)
}
在demo20.go文件中我声明并初始化了一个元素类型为int容量为3的通道ch1并用三条语句向该通道先后发送了三个元素值21和3
这里的语句需要这样写依次敲入通道变量的名称比如ch1)、接送操作符<-以及想要发送的元素值比如2并且这三者之间最好用空格进行分割
这显然表达了这个元素值将被发送该通道这个语义由于该通道的容量为3所以我可以在通道不包含任何元素值的时候连续地向该通道发送三个值此时这三个值都会被缓存在通道之中
当我们需要从通道接收元素值的时候同样要用接送操作符<-只不过这时需要把它写在变量名的左边用于表达要从该通道接收一个元素值的语义
比如<-ch1这也可以被叫做接收表达式在一般情况下接收表达式的结果将会是通道中的一个元素值
如果我们需要把如此得来的元素值存起来那么在接收表达式的左边就需要依次添加赋值符号=或:=和用于存值的变量的名字。因此语句elem1 := <-ch1会将最先进入ch1的元素2接收来并存入变量elem1。
现在我们来看一道与此有关的题目今天的问题是对通道的发送和接收操作都有哪些基本的特性
这个问题的背后隐藏着很多的知识点我们来看一下典型回答
它们的基本特性如下
对于同一个通道发送操作之间是互斥的接收操作之间也是互斥的
发送操作和接收操作中对元素值的处理都是不可分割的
发送操作在完全完成之前会被阻塞接收操作也是如此
问题解析
我们先来看第一个基本特性 在同一时刻Go语言的运行时系统以下简称运行时系统只会执行对同一个通道的任意个发送操作中的某一个
直到这个元素值被完全复制进该通道之后其他针对该通道的发送操作才可能被执行
类似的在同一时刻运行时系统也只会执行对同一个通道的任意个接收操作中的某一个
直到这个元素值完全被移出该通道之后其他针对该通道的接收操作才可能被执行即使这些操作是并发执行的也是如此
这里所谓的并发执行你可以这样认为多个代码块分别在不同的goroutine之中并有机会在同一个时间段内被执行
另外对于通道中的同一个元素值来说发送操作和接收操作之间也是互斥的例如虽然会出现正在被复制进通道但还未复制完成的元素值但是这时它绝不会被想接收它的一方看到和取走
这里要注意的一个细节是元素值从外界进入通道时会被复制更具体地说进入通道的并不是在接收操作符右边的那个元素值而是它的副本
另一方面元素值从通道进入外界时会被移动这个移动操作实际上包含了两步第一步是生成正在通道中的这个元素值的副本并准备给到接收方第二步是删除在通道中的这个元素值
顺着这个细节再来看第二个基本特性 这里的不可分割的意思是它们处理元素值时都是一气呵成的绝不会被打断
例如发送操作要么还没复制元素值要么已经复制完毕绝不会出现只复制了一部分的情况
又例如接收操作在准备好元素值的副本之后一定会删除掉通道中的原值绝不会出现通道中仍有残留的情况
这既是为了保证通道中元素值的完整性也是为了保证通道操作的唯一性对于通道中的同一个元素值来说它只可能是某一个发送操作放入的同时也只可能被某一个接收操作取出
再来说第三个基本特性 一般情况下发送操作包括了复制元素值放置副本到通道内部这两个步骤
在这两个步骤完全完成之前发起这个发送操作的那句代码会一直阻塞在那里也就是说在它之后的代码不会有执行的机会直到这句代码的阻塞解除
更细致地说在通道完成发送操作之后运行时系统会通知这句代码所在的goroutine以使它去争取继续运行代码的机会
另外接收操作通常包含了复制通道内的元素值”“放置副本到接收方”“删掉原值三个步骤
在所有这些步骤完全完成之前发起该操作的代码也会一直阻塞直到该代码所在的goroutine收到了运行时系统的通知并重新获得运行机会为止
说到这里你可能已经感觉到如此阻塞代码其实就是为了实现操作的互斥和元素值的完整
下面我来说一个关于通道操作阻塞的问题
知识扩展
问题1发送操作和接收操作在什么时候可能被长时间的阻塞
先说针对缓冲通道的情况如果通道已满那么对它的所有发送操作都会被阻塞直到通道中有元素值被接收走
这时通道会优先通知最早因此而等待的那个发送操作所在的goroutine后者会再次执行发送操作
由于发送操作在这种情况下被阻塞后它们所在的goroutine会顺序地进入通道内部的发送等待队列所以通知的顺序总是公平的
相对的如果通道已空那么对它的所有接收操作都会被阻塞直到通道中有新的元素值出现这时通道会通知最早等待的那个接收操作所在的goroutine并使它再次执行接收操作
因此而等待的所有接收操作所在的goroutine都会按照先后顺序被放入通道内部的接收等待队列
对于非缓冲通道情况要简单一些无论是发送操作还是接收操作一开始执行就会被阻塞直到配对的操作也开始执行才会继续传递由此可见非缓冲通道是在用同步的方式传递数据也就是说只有收发双方对接上了数据才会被传递
并且数据是直接从发送方复制到接收方的中间并不会用非缓冲通道做中转相比之下缓冲通道则在用异步的方式传递数据
在大多数情况下缓冲通道会作为收发双方的中间件正如前文所述元素值会先从发送方复制到缓冲通道之后再由缓冲通道复制给接收方
但是当发送操作在执行的时候发现空的通道中正好有等待的接收操作那么它会直接把元素值复制给接收方
以上说的都是在正确使用通道的前提下会发生的事情下面我特别说明一下由于错误使用通道而造成的阻塞
对于值为nil的通道不论它的具体类型是什么对它的发送操作和接收操作都会永久地处于阻塞状态它们所属的goroutine中的任何代码都不再会被执行
注意由于通道类型是引用类型所以它的零值就是nil换句话说当我们只声明该类型的变量但没有用make函数对它进行初始化时该变量的值就会是nil我们一定不要忘记初始化通道
你可以去看一下demo21.go我在里面用代码罗列了一下会造成阻塞的几种情况
问题2发送操作和接收操作在什么时候会引发panic
对于一个已初始化但并未关闭的通道来说收发操作一定不会引发panic但是通道一旦关闭再对它进行发送操作就会引发panic
另外如果我们试图关闭一个已经关闭了的通道也会引发panic注意接收操作是可以感知到通道的关闭的并能够安全退出
更具体地说当我们把接收表达式的结果同时赋给两个变量时第二个变量的类型就是一定bool类型它的值如果为false就说明通道已经关闭并且再没有元素值可取了
注意如果通道关闭时里面还有元素值未被取出那么接收表达式的第一个结果仍会是通道中的某一个元素值而第二个结果值一定会是true
因此通过接收表达式的第二个结果值来判断通道是否关闭是可能有延时的
由于通道的收发操作有上述特性所以除非有特殊的保障措施我们千万不要让接收方关闭通道而应当让发送方做这件事这在demo22.go中有一个简单的模式可供参考
总结
今天我们讲到了通道的一些常规操作包括初始化发送接收和关闭通道类型是Go语言特有的所以你一开始肯定会感到陌生其中的一些规则和奥妙还需要你铭记于心并细心体会
首先是在初始化通道时设定其容量的意义这有时会让通道拥有不同的行为模式对通道的发送操作和接收操作都有哪些基本特性也是我们必须清楚的
这涉及了它们什么时候会互斥什么时候会造成阻塞什么时候会引起panic以及它们收发元素值的顺序是怎样的它们是怎样保证元素值的完整性的元素值通常会被复制几次等等
最后别忘了通道也是Go语言的并发编程模式中重要的一员
思考题
我希望你能通过试验获得下述问题的答案
通道的长度代表着什么它在什么时候会通道的容量相同
元素值在经过通道传递时会被复制那么这个复制是浅表复制还是深层复制呢
戳此查看Go语言专栏文章配套详细代码