channel管道
channel
如果说goroutine是并发体的话,那么channels则是他们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发生值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int.
使用内置的make函数,我们可以创建一个channel:
ch := make(chan int)
和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象,和其它的引用类型一样,channel的零值也是nil。
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接受操作的goroutine。发送和接收两个操作都使用<-运算符。在发送语句中。<-运算符分割channel和要发送的值。在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。
ch <- x // send
x = <-ch //receive
<-ch // receive
Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。
使用内置的close函数就可以关闭一个channel:
Close(ch)
以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
不带缓存的Channels
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒唤醒发送者goroutine之前。
在上面执行main函数的goroutine中将标准输入复制到server,因此当客户端程序关闭标准输入时,后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出,我们使用了一个channel来同步两个goroutine
func main(){
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil{
log.Fatal(err)
}
done := make(chan struct{})
// 开启协程往管道中写入
go func(){
io.Copy(os.Stdout, conn)
log.Println("done")
done <- struct{}{} //空结构体发送到done通道中
}()
mustCopy(conn, os.Stdin)
conn.Close()
// 同步获取,阻塞
<-done // wait for background goroutine to finish
}
当用户关闭标准输入,主goroutine中的mustCopy函数调用将返回,然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件(end-of-file)结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个"read from closed connection"(从关闭的连接读)类似的错误,因此我们临时移除了错误日志语句。
在后台goroutine返回之前,它先打印一个日志信息,然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出"done"消息。
基于channels发送消息有两个重要方面:首先每个消息都有一个值但是有时候通讯的事实和发生的时刻也同样重要,当我们更希望强调通讯发生的时刻时,我们将它称为消息事件。有些消息事件并不携带额外的消息,它仅仅是用作两个goroutine之间的同步,这时候我们可以用struct{}空结构体作为channels元素的类型,虽然也可以使用bool或int类型实现同样的功能,done<- 1语句也比done <- struct{}更短。
串联的Channels(Pipeline)
Channels也可以用于多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来
第一个goroutine是一个计数器,用于生成0,1,2...形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个平方的程序,对收到的每个整数平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。为了保持例子清晰,我们有意选择了非常简单的函数
func main(){
naturals := make(chan int)
squares := make(chan int)
// Counter
go func(){
for x :=0;;x++{
naturals <- x // 写
}
}()
// Squarer
go func(){
for {
x := <-naturals // 取
squares <- x * x // 写
}
}()
for {
fmt.Println(<-squares) // 取
}
}
如您所料,上面的程序将生成0,1,4,9...形式的无穷数列。像这样的串联Channels的管道(Pipelines)可以用在需要长时间运行的服务中,每个长时间运行的goroutine可能会包含一个死循环,在不同goroutine的死循环内部使用串联的Channels来通信。但是,如果我们希望通过Channels只发送有限的数列该如何处理呢?
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:
close(naturals)
当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会理解返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列,然后将它们发送给打印者goroutine.
没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接受的第二个结果是一个布尔值ok,true表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel
// squarer
go func(){
for {
x, ok := <-naturals
if !ok{
break
}
squares < x *x
}
close(squares)
}()
// for ... range channel
func main(){
naturals := make(chan int)
squares := make(chan int)
// Counter
go func(){
for x :=0;x<100;x++{
naturals <- x
}
close(naturals)
}()
go func(){
for x:= range naturals{
squares <- x*x
}
close(squares)
}()
for x := range squares{
fmt.Println(x)
}
}
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被go语言的垃圾自动回收器回收。
试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制。
单方向channel
func counter(out chan<-int){ // 只写通道
for x:=0;x<100;x++{
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int){ //只写通道,只读通道
for v:= range in{
out <- v* v
}
close(out)
}
func printer(in <-chan int){
for v := range in{
fmt.Println(v)
}
}
func main(){
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
带缓存的channels
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。
下面的语句创建了一个可以持有三个字符串元素的带缓存Channel.
ch = make(chan string, 3)
向缓存Channel发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
我们可以在无阻塞的情况下连续向新创建的channel发送三个值:
ch <- "A"
ch <- "B"
ch <- "C"
如果再往里面写的话,此时channel的内部缓存队列是满的,将发送阻塞
ch <- "D" // 阻塞
如果我们接收一个值
fmt.Println(<-ch) // "A"
那么channel的缓存队列将不是满的也不是空的。因此对该channel执行的发送接收操作都不会发生阻塞。
通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。
在某些特殊的情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:
fmt.Println(cap(ch)) //"3"
同样,对于内置的len函数,如果传入的是channel,那么将返回channel内部缓存队列中有效元素的个数,因为在并发程序中该信息会随着接受操作而失效,但是它对某些故障诊断和性能优化会有帮助。
fmt.Println(len(ch)) // "2"
在继续执行两次接受操作后channel内部的缓存队列将又成为空的,如果有第四个接收操作将发生阻塞:
fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"
在这个例子中,发送和接收操作都发生在同一个goroutine中,但是在真实的程序中它们一般由不同的goroutine执行。
新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用。虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其它goroutine从channel接收,发送者--或许是整个程序----将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了
下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。(多个goroutines并发地向同一个channel发送数据,或从同一个channel接收数据都是常见的用法。)
func mirroredQuery() string{
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
return <-responses // return the quickest response
}
func request(hostname string) (response string) { /*...*/ }
如果使用了无缓存的channel,那么两个慢的goroutine将会因为没有人接收而被永远卡住,这种情况,称为goroutine泄露,这将是一个bug.和垃圾变量不同,泄露的goroutine并不会自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样的,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。
channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘培,一个上糖衣,还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境,每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它:这类似于在一个无缓存的channel上进行沟通。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中;这类似于将channel的缓存队列的容量设置为1;只要每个厨师的平均工作效率更近,那么其中大部分的传输工作将是迅速的,个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间---也是就更大容量的缓存队列---将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短暂地休息,然后再加快赶上进度而不影响其它人。
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。
生产线的隐喻对于理解channels和goroutine的工作机制是很有帮助的。例如,如果第二阶段是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第三阶段厨师的需求。要解决这个问题,我们可以再雇佣另一个厨师来帮助完成第二阶段的工作,他执行相同的任务但是独立工作,这类似于基于相同的channels创建另一个独立的goroutine.
sync.WaitGroup
func makeThumbnails6(filenames <-chan string) int64{
sizes := make(chan int64)
var wg sync.WaitGroup
for f := range filenames{
wg.Add(1)
// worker
go func(f string){
defer wg.Done()
thumb, err := thumbnail.ImageFile(f)
if err != nil{
log.Println(err)
return
}
info, _ := os.Stat(thumb)
size <- info.Size()
}(f)
}
// closer
go func(){
wg.Wait()
close(sizes)
}()
var total int64
for size := range sizes{
total += size
}
return total
}
注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中;否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确的被减掉。上面的程序代码结构是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。
sizes channel携带了每一个文件的大小到man goroutine,在main goroutine中使用了range loop来计算总和,观察一下我们是怎么创建一个closer goroutine,并让其所在worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何东西去关闭这个channel,这个循环就永远都不会终止。
channel管道的更多相关文章
- NIO Channel 管道
Java NIO的通道类似流,但又有些不同: 既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的. 通道可以异步地读写. 通道中的数据总是要先读到一个Buffer,或者总是要从一个Bu ...
- Go语言 channel 管道 阻塞 死锁 经典问题
建议阅读:14.2协程间的信道 问题:为什么代码1会报死锁的错误,而代码2不会报错? 代码1: package main import ( "fmt" ) func main() ...
- NIO流—理解Buffer、Channel概念和NIO的读写操作
NIO流与IO流的区别 面向流与面向块 IO流是每次处理一个或多个字节,效率很慢(字符流处理的也是字节,只是对字节进行编码和解码处理). NIO流是以数据块为单位来处理,缓冲区就是用于读写的数据块.缓 ...
- go-goroutine 和 channel
goroutine 和 channel goroutine-看一个需求 需求:要求统计 1-9000000000 的数字中,哪些是素数? 分析思路: 1) 传统的方法,就是使用一个循环,循环的判断各个 ...
- golang(8):channel读写 & goroutine 通信
goroutine 1.进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独 ...
- Netty入门(二):Channel
前言 Netty系列索引: 1.Netty入门(一):ByteBuf 2.Netty入门(二):Channel 在Netty框架中,Channel是其中之一的核心概念,是Netty网络通信的主体,由它 ...
- golang 中 channel 的详细使用、使用注意事项及死锁分析
目录 1.什么是 channel,介绍管道 2.channel 的基本使用 3.channel 的使用场景 4.使用 channel的注意事项及死锁分析 什么是 channel 管道 它是一个数据管道 ...
- javaIO框架小析
IO即数据读写.数据是应用的中心要素,而数据读写的能力和可扩展性是编程平台的基础支撑. 概念框架 方式: 字节流 Byte 和 字符流 Char 方向: 输入 Input 和 输出 Output : ...
- Go从入门到精通(一)go语言初始
一.第一个go程序 package main import ( "fmt" ) func main(){ fmt.Println("hello world") ...
- Go学习笔记(一)Let's 干
加 Golang学习 QQ群共同学习进步成家立业 ^-^ 群号:96933959 简介 Go是Google开发的一种 静态强类型.编译型,并发型,并具有垃圾回收功能的编程语言.为了方便搜索和识 ...
随机推荐
- [百度贴吧]部分CPU的SPEC2006int 结果
这些测试成绩基本上是本人自己测试的结果.下表中有来自spec官网的两个成绩,因为测试年份较早,系统环境和编译器都较老,测试成绩本人实测的还差,所以仅作为参考.部分测试启用了自动并行和附加的优化库,是为 ...
- [转帖]使用 Crash 工具分析 Linux dump 文件
前言 Linux 内核(以下简称内核)是一个不与特定进程相关的功能集合,内核的代码很难轻易的在调试器中执行和跟踪.开发者认为,内核如果发生了错误,就不应该继续运行.因此内核发生错误时,它的行为通常被设 ...
- zabbix监控进程和监控日志
zabbix监控进程和监控日志 文章目录 zabbix监控进程和监控日志 一.自定义监控进程 1.新建脚本存放目录 2.修改zabbix_agentd.conf文件 3.zabbix server端进 ...
- 为什么Kubernetes和容器与机器学习密不可分?
原文出自infosecurity 作者:Rebecca James 京东云开发者社区编译 当前,数字化转型的热潮在IT领域发展的如火如荼,越来越多的企业投身其中,机器学习和人工智能等现代技术的融合在公 ...
- window.addEventListener注册滚动scroll事件不生效
先了解一下滚动事件触发的条件 视图或者一个元素在滚动时,会触发元素的 scroll 事件. 备注: 在 iOS UIWebViews 中,滚动进行时不会触发 scroll 事件:只有当滚动结束后事件才 ...
- js获取字符串最后几位字符数
截取字符串 为什要截取字符串呢??? 因为有些时候,我们需要判断某一个字符串中是不是,含有特定的字符 substring(a)从起始位置开始(包含a这个位置),一直到字符串的末尾(截取字符串最后6个) ...
- 【三】强化学习之PaddlePaddlle-Notebook、&pdb、ipdb 调试---及PARL框架
相关文章: [一]飞桨paddle[GPU.CPU]安装以及环境配置+python入门教学 [二]-Parl基础命令 [三]-Notebook.&pdb.ipdb 调试 [四]-强化学习入门简 ...
- gym中的discrete类、box类和multidiscrete类简介和使用
相关文章: Box() dict()可用于创建连续的空间:OpenAI Gym Discrete和Box spaces同时存在,代码该怎么写:gym中各种离散连续写法 解读gym中的action_sp ...
- 我管你什么okr还是kpi,PPT轻松交给你
word一键转ppt 通过[文件]→[选项]→[快速访问工具栏],选择[不在功能区中的命令],找到[发送到Microsoft PowerPoint ],单击[添加]后再[确定] 调出功能按钮后,就可以 ...
- ESXi6.5导入虚拟机提示缺少所需的磁盘镜像
环境 esxi6.7 错误提示 解决方案 原因:这是因为导出虚拟机的时候,没有把"CD/DVD驱动器"删掉,在导入的时候,找不到这个磁盘映像. 编辑.ovf文件,找到ovf:hre ...