管道(channel)
上一节我们介绍了Golang中如何使用goroutine
实现并发,本节我们介绍下channel
,即管道
,来看看Golang中是怎么使用channel
来进行goroutine
之间的通信的。
什么是channel
channel
channel
可以看作是goroutine
互相通信的管道。就像水可以从管道的一端流向另一端,数据也可以从channel
的一端流向另一端。
声明channel
channel
每个channel
都需要绑定一个数据类型,这个数据类型就是channel
可以传输的数据的类型,channel
不允许传输其他任何数据类型。
一个channel
的类型声明如下:
channel
的零值为nil
。nil
channel没有任何用处,我们需要使用make
来初始化一个channel
。
这里声明的channel
未手动初始化,因此自动初始化为零值nil
。后面的判断条件a == nil
成立后执行if
语句内部的语句。我们使用make(chan int)
创建了一个chan int
类型的channel
。该程序输出如下:
当然,我们也可以声明channel
的同时进行手动初始化:
发送 & 接收数据
从channel
中发送、接收数据的语法如下:
箭头的指向形象的表明了是从channel
中接收数据,还是向challel
发送数据。data := <- a
中箭头从channel
指出,表示从channel
a
中读取数据并赋值给变量data
。a <- data
中箭头指向一个channel
,因此是向channel
a
中写入数据data
。
向channel
发送、从channel
接收数据会阻塞goroutine
执行
channel
发送、从channel
接收数据会阻塞goroutine
执行向channel
发送数据,以及从channel
接收数据会阻塞当前goroutine
的执行。这句话怎么理解呢?其实很简单,无论我们是向channel
发送数据,还是从channel
接收数据,无非是通过类似于data := <- a
或者a <- data
的语句来完成,而语句本身的执行肯定是位于一个goroutine
内。如果执行的是a <- data
,即向一个channel
发送数据,这时执行流会阻塞,直到有其他goroutine
从这个channel
中读取了刚刚写入的数据。类似的,如果执行的是data := <- a
,即从一个channel
中读取数据,这时执行流也也会阻塞,直到有其他goroutine
向这个channel
中写入数据。
channel
就是基于这个特性而不是类似于其他编程语言中的锁来实现不同goroutine
之间的高效通信的。
channel
使用举例
channel
使用举例下面我们通过例子来看下goroutine
是如何使用channel
进行通信的。
我们首先还用上一节的那个简单例子:
该程序使用了time.Sleep
来让主协程等待hello
协程的执行,下面我们改用正规做法,即使用channel
来同步主协程和hello
子协程的执行:
这里我们通过done := make(chan bool)
创建了一个channel
,该channel
的类型为chan bool
,即支持传递bool
类型的数据。我们将该channel
作为入参传给了hello
协程,因此该协程内部可以给该channel
传递数据。注意我们开启协程之后的一行代码:<-done
。这行代码是从done
channel
内读取数据,因此会阻塞主协程的执行,直到该channel
内有数据能够读到为止,直到读到数据主协程才会继续执行。
这里的<-done
仅仅等待done
channel
有数据可读,并没有将读到的数据赋值给某个变量,这是完全合法的。比如这里我们仅仅是为了找到同步点,而并不需要读到什么数据。
协程hello
内部先打印了Hello world goroutine
,然后向done
channel
内部写入了一个bool
类型的数据true
,这时主协程收到了这个数据,不再被阻塞,接着打印main function
。
该程序的执行结果如下,注意是稳定输出:
我们再修改下前面的程序,在hello
协程里面加个sleep
,让hello
协程的执行时间变长点,看看主协程会不会等待该子协程向channel
内写入数据。
这里我们在hello
协程内sleep
了四秒钟才向channel
内写入了数据,由于<-done
是阻塞的,因此仍然需要等待channel
中写入数据,该程序执行结果如下,也是稳定输出:
下面我们再举个例子,以更好的理解goroutine
和channel
。这个程序用来计算一个数子的各个位的平方和,各个位的立方和,再将二者加起来。比如输出是123,那么算法如下:
squares = (1 1) + (2 2) + (3 3) cubes = (1 1 1) + (2 2 2) + (3 3 * 3) output = squares + cubes = 50
我们实现上将平方和、立方和的计算放到不同的协程内去执行,在主协程内等待两个协程执行完成后再将两个协程的计算结果想加。
这里squares, cubes := <-sqrch, <-cubech
会等待两个协程将计算结果写到对应的channel
内。该程序执行结果如下:
死锁
使用channel
时需要注意别陷入死锁
状态。如果某个goroutine
向某个channel
发送数据,那么需要某个其他goroutine
接收该channel
的数据,否则会发送运行时错误:deadlock
。类似的,如果某个goroutine
正在等待从某个channel
中接收数据,那么需要某个其他goroutine
向该channel
中写入数据,否则也会发送运行时错误:deadlock
。
这里我们创建了一个channel
ch,类型为chan int
。然后我们向该channel
中写入了一个值5
。但是在该程序中并没有其他goroutine
接收该channel
中的数据。因此会报运行时错误:
可读写channel
、读channel
、写channel
channel
、读channel
、写channel
前面我们讨论的channel
都是既可读、又可写的。我们也可以创建仅支持可读,或仅支持可写的channel
。
这里我们创建了一个写channel
sendch
,从箭头指向也能看出来这是一个写channel
。fmt.Println(<-sendch)
尝试从该channel
中读取数据,这是不允许的,Golang不允许从写channel
中读取数据,因此报以下错误:
尽然channel
是用来做通信、数据传输用的,那么仅支持可写、或仅支持可读有什么意义呢?答案是channel 转换
。我们可以将读写channel
转换为读channel
或者写channel
;但反之不行,不能将读channel
或者写channel
转换为读写channel
。
这里我们首先创建了一个读写channel
chnl
,然后将该读写channel
传给goroutine
sendData
,sendData
通过参数将该读写channel
转换为了写channel
,因此sendData
内部该channel
是仅可写的,而在main channel
中是既可读又可写的。该程序输出如下:10
。
关闭channel
channel
channel
的发送方有权关闭channel
,关闭channel
会通知数据接收方,告知对方数据已发送完成,没有后续数据了。
数据接收方可通过如下方式来判断channel
是否已经关闭:
如果是正常的数据发送,则ok
为true
,如果ch
被数据发送方关闭了,则ok
为false
,并且数据v
接收到的值为对应数据类型的默认零值。
这里的producer
协程向channel
中写入了0到9,接着关闭了channel
。主函数通过一个无限循环来不断的读取channel
中的数据,通过ok
字段来判断channel
是否关闭,如果为false
则表示关闭。该程序执行结果如下:
for range
前面使用无限for循环来从channel
中读取数据,Golang提供了一种更加方便的方式来不断的读取channel
中的数据,直到channel
关闭。
这里for range
语句从channel
ch
中不断读取数据,直到channel
关闭。该程序输出如下:
我们使用for range
重构一下前面计算平方和、立方和的程序,细心的话会发现前面calcSquares
函数和calcCubes
函数中有几行数字循环的样板代码,我们使用for range
重构下:
我们将样板代码抽成了一个函数digits
,在calcSquares
函数和calcCubes
函数中分别创建了协程来不断输出迭代出来的数字,直到协程被关闭。该程序输出结果为:
Last updated
Was this helpful?