channel的基本操作

时间:2024-11-13 20:41:20

2019独角兽企业重金招聘Python工程师标准>>>

激动,终于学到了Go最初吸引我的知识了。

channel作为Go语言最有特色的数据类型,和goroutine并驾齐驱,共同代表了Go语言独有的并发编程模式和编程哲学:

Don't communicate by sharing memory; share memory by communicating.(不要通过共享内存来通信,而应该通过通信来共享内存。)——Rob Pike Go语言主要创造者之一。

channel是后半句话的完美实现。我们可以利用通道在多个goroutine之间传递数据。

通道类型的值本身就是并发安全的,这是Go语言自带的、唯一一个可以满足并发安全性的类型。

创建channel

c := make(chan int)

(len(c))  //0
(cap(c))  //0

make(chan int):chan是Go的关键字,表示通道,int说明了该通道的元素类型。

make(chan int , 2)
(len(c))  //0
(cap(c))  //2

这行代码表示创建一个容量为2的元素类型为int的通道。

容量这个参数(不能小于0),是可选的,所谓的容量是指通道可以醉倒缓存多少个元素值。通道的长度是指,当前通道中元素的数量。

通道的分类

当容量为0时,叫做非缓冲通道。 当容量大于0时,叫做缓冲通道。

通道的底层数据结构是环形链表。

一个通道相当于一个先进先出的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送进通道的元素值一定会先被接收。

元素值的发送和接收都需要用到接送操作符<-

//发送表达式

c := make(chan int ,1)
c <- 1

//接收表达式

value,ok := <- c

对通道的发送和接收操作都有哪些基本的特性?

回答:

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
  2. 发送操作和接收操作过程中对元素值的处理都是不可分割的
  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此。

对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的

同一时刻,有多个对同一个通道的发送操作,Go语言的运行时系统只会执行其中一个。知道这个元素值被复制进该通道之后,其他的针对该通道的发送操作才可能被执行。

类似的,在同一个时刻,运行时系统也是会执行对同一个通道的人一个接收操作中的某一个。直到这个元素值被完全移出该通道之后,其他的对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。

另外,对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。正在被复制但是还没有完全复制进通道的元素值,是不可能被想接收它的一方看到和取走。

注意这里的一个细节:元素值从外界被复制进通道。意思是进入通道的是副本。这里的复制是浅复制。

元素值从通道进入外界时,这个操作包含了两步:

  • 生成通道中这个元素值的副本,并准备给到接收方
  • 删除通道中的这个元素值。

发送操作和接收操作过程中对元素值的处理都是不可分割的

发送操作要么还没有复制元素值,要么已经复制完毕,绝对不会出现只复制了一部分的情况。

接收操作的复制通道中的副本,将副本发送给接收方,删除通道中的元素值这一系列动作是一气呵成的,绝不会被打断。

发送操作在完全完成之前会被阻塞。接收操作也是如此。

发送操作

一般情况下,发送操作包括了“复制元素副本”和“放置副本到通道内部”这两个步骤,这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。这句代码之后的代码不会有执行的机会,直到这句代码的阻塞解除。

更标准的说法是:在通道完成发送操作之后,运行时系统会通知这句代码所在的goroutine,使这个goroutine去争取继续运行代码的机会。

接收操作

接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。

在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine收到了运行时系统的通知并重新获得运行机会为止。

发送操作和接收操作在什么时候可能被长时间的阻塞?

缓冲通道

通道已满:

对它的所有发送操作都会被阻塞,发送操作所在的goroutine会顺序地进入通道内部的“发送等待队列”,直到通道中有元素被接收。这时,通道会优先通知最早等待发送的goroutine,这个goroutine会再次执行发送操作。

通道已空

对它的所有接收操作都会被阻塞,接收操作所在的goroutine会按照先后顺序被放入通道内部的“接收等待队列”,直到通道中有新的元素值出现。通道就会通知最早等待的那个接收操作所在的goroutine,并使它再次执行接收操作。

非缓冲通道

无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。并且,数据是直接从发送发复制到接收方的,中间不会用非缓冲通道做中转。

由此可见:非缓冲通道是在用同步的方式传递数据。也就是说,只有接收双发对接上了,数据才会被传递。

相比之下,缓冲通道则是在用异步的方式传递数据。在大多数情况下,缓冲通道会作为收发双方的中间件,元素值会先从发送发复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行的时候如果发现空的通道中,正好有等待的接收方,那么它会直接把元素复制给接收方。

值为nil的通道

不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态,它们所属的goroutine中的任何代码,都不再会别执行。

发送操作和接收操作在什么时候会引发panic?

  1. 通道一旦关闭,再对它进行发送操作,就会引发panic。
  2. 如果试图关闭一个已经关闭了的通道,也会引发panic。

注意,接收方是可以感知到通道的关闭的,并能够安全退出。

value, ok := <- c  //接收表达式

如果ok是false,说明通道已经关闭,并且没有元素可取了。

有一种情况:通道关闭时,里面还有元素未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素,第二个结果一定是true。

考虑上面的情况,我们不能依靠接收表达式的第二个结果值来判断通道是否关闭。

考虑到通道的收发操作有如上的特性,所以除非有特殊的保障措施,否则,我们千万不要让接收方关闭通道,而应该让发送发关闭通道。