Golang编程之Channel

133 阅读6分钟

要想了解 Channel这种Go编程语言中的特有的数据结构,我们要追溯到 CSP 模型,学习下它的历史,以及它对 Go 创始人设计 Channel 类型的影响。

CSP 是Communicating Sequential Process 的简称,中文直译为通信顺序进程,或者叫做交换信息的循序进程,是用来描述并发系统中进行交互的一种模式。

CSP 最早出现于计算机科学家 Tony Hoare 在1978 年发表的论文中。最初,论文中提出的 CSP 版本在本质上不是一种进程演算,而是一种并发编程语言,但之后又经过了一系列的改进,最终发展并精炼出CSP 的理论。CSP允许使用进程组件来描述系统,它们独立运行,并且只通过消息传递的方式通信.

Channel 类型是 Go 语言内置的类型,你无需引入某个包,就能使用它。虽然Go 也提供了传统的并发原语,但是它们都是通过库的方式提供的,你必须要引入 sync 包或者atomic包才能使用它们,而 Channel 就不一样了,它是内置类型,使用起来非常方便。

Channel和Go 的另一个独特的特性 goroutine 一起为并发编程提供了优雅的、便利的、与传统并发控制不同的方案,并演化出很多并发模式。接下来,我们就来看一看 Channel的应用场景。

使用Go语言的哲学,执行业务处理的 goroutine 不要通过共享内存的方式通信,而是要通过Channel通信的方式分享数据。

从Channel 的历史和设计哲学上,我们就可以了解到,Channel 类型和基本并发原语是有竞争关系的,它应用于并发场景,涉及到 goroutine 之间的通讯,可以提供并发的保护,等等。

综合起来,我把 Channel 的应用场景分为五种类型。这里你先有个印象,这样你可以有目的地去学习 Channel 的基本原理。下节课我会借助具体的例子,来带你掌握这几种类型。

1.数据交流:当作并发的 buffer 或者 queue,解决生产者- 消费者问题。多个 goroutine可以并发当作生产者 (Producer) 和消费者 (Consumer) 。 2.数据传递:一个 goroutine 将数据交给另一个 goroutine,相当于把数据的拥有权(引用)托付出去 3.信号通知:一个 goroutine 可以将信号(closing、closed、data ready 等)传递给另一个或者另一组 goroutine 4.任务编排:可以让一组 goroutine 按照一定的顺序并发或者串行的执行,这就是编排的功能。 5.锁:利用Channel 也可以实现互斥锁的机制

基本用法

你可以往 Channel中发送数据,也可以从 Channel 中接收数据,所以,Channel类型(为了说起来方便,我们下面都把 Channel 叫做 chan)分为只能接收、只能发送、既可以接收又可以发送三种类型。下面是它的语法定义:

chan string   // 可以发送接收string
chan<- struct{} //只能发送struct
<-chan int //只能从chan接收int

我们把既能接收又能发送的 chan 叫做双向的 chan,把只能发送和只能接收的 chan 叫做单“<-”表示单向的 chan,如果你记不住,我告诉你一个简便的方法: 这向的 chan。其中,个箭头总是射向左边的,元素类型总在最右边。如果箭头指向 chan,就表示可以往 chan 中塞数据;如果箭头远离chan,就表示chan 会往外吐数据

chan 中的元素是任意的类型,所以也可能是 chan 类型,我来举个例子,比如下面的 chan类型也是合法的:

chan (<-chan int)
chan<- chan int
chan<- <-chan int
<-chan <-chan int

可是,怎么判定箭头符号属于哪个 chan 呢? 其实,“<-”有个规则,总是尽量和左边的chan 结合。

通过make,我们可以初始化一个chan,未初始化的 chan 的零值是nil。你可以设置它的容量,比如下面的chan的容量是10,我们把这样的chan 叫做 buffered chan;如果没有设置,它的容量是0,我们把这样的 chan 叫做unbuffered chan。

make(chan int, 10)

如果chan 中还有数据,那么,从这个 chan 接收数据的时候就不会阻塞,如果 chan 还未满(“满”指达到其容量),给它发送数据也不会阻塞,否则就会阻塞。unbuffered chan 只有读写都准备好之后才不会阻塞,这也是很多使用unbuffered chan 时的常见 Bug。

还有一个知识点需要你记住: nil是 chan 的零值,是一种特殊的 chan,对值是nil的chan的发送接收调用者总是会阻塞

下面,我来具体给你介绍几种基本操作,分别是发送数据、接收数据,以及一些其它操作。学会了这几种操作,你就能真正地掌握Channel的用法了

如果 chan 中还有数据,那么,从这个 chan 接收数据的时候就不会阻塞,如果chan 还未满(“满”指达到其容量),给它发送数据也不会阻塞,否则就会阻塞。unbuffered chan 只有读写都准备好之后才不会阻塞,这也是很多使用unbuffered chan 时的常见Bug。

还有一个知识点需要你记住: nil是chan 的零值,是一种特殊的 chan,对值是nil的chan的发送接收调用者总是会阻塞。 下面,我来具体给你介绍几种基本操作,分别是发送数据、接收数据,以及一些其它操作。学会了这几种操作,你就能真正地掌握Channel的用法了

1.发送数据

往chan中发送一个数据使用“ch<-”发送数据是一条语句: ch <- 100

2.接收数据

a := <- ch
foo(<-ch)
<-ch //丢弃一条数据

接收数据时,还可以返回两个值。第一个值是返回的 chan 中的元素,很多人不太熟悉的是第二个值。第二个值是 bool 类型,代表是否成功地从 chan 中读取到一个值,如果第二个参数是false,chan 已经被 close 而且 chan 中没有缓存的数据,这个时候,第一个值是零值。所以,如果从 chan 读取到一个零值,可能是 sender 真正发送的零值,也可能是 closed 的并且没有缓存元素产生的零值.

3.其它操作

Go 内建的函数 close、cap、len 都可以操作 chan 类型: cse 会把 chan 关闭掉,cap 返回chan的容量,len 返回chan 中缓存的还未被取走的元素数量。

send 和recv 都可以作为 select 语的 cas clause,如下面的例子

func main() {

	var ch = make(chan int10)
	for i := 0;i<10;i++ {
		select {
			case ch <- i:
			case V := <-ch:
				fmt.PrintIn(v)
		}
	}
}

chan 还可以应用于 for-range 语句中,比如

for v := range ch {
	fmt.Println(v)
}

for range ch {
	//清空channel
}