转自:http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/

在进入正式内容前,我这里先顺便转发一则消息,那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面,国内go程序员与爱好者们可以到"Golang中 国",即golangtc.com去下载go 1.3.2版本。

Go这门语言也许你还不甚了解,甚至是完全不知道,这也有情可原,毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下,如 果没有涉及到“docker”字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作:《 Docker – 从入门到实践》。

据我了解,目前国内试水Go语言开发后台系统的大公司与初创公司日益增多,比如七牛、京东、小米,盛大,金山,东软,搜狗等,在这里我们可以看到一些公司的Go语言应用列表,并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中,不过目前来看多以Go入 门与基础为主题,Go idioms、tips或Best Practice的Share并不多见,想必国内的先行者、布道师们还在韬光养晦,积攒经验,等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台,比如Golang技术大会之类的的平台。

在国外,虽然Go也刚刚起步,但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多,除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外,由 Gopher Academy 发起的GopherCon 会议也于今年第一次举行,并放出诸多高质量资料,在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕,估计后续这两个大会将撑起Golang技术分享 的旗帜。

言归正传,这里要写的东西并非原创,自己的Go仅仅算是入门级别,工程经验、Best Practice等还谈不上有多少,因此这里主要是针对GopherCon2014上的“舶来品”的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验,这里针对其分享的内容,记录一些学习体会和理解,并结合一些外延知识,也可以算是一种学习笔记吧,仅供参考。

一、Golang并发基础理论

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并不多,毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。

维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别:

Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言,又有一些根本上的不同之处:
    – CSP模型处理过程是匿名的,而Actor模型中的Actor则具有身份标识。
    – CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。相反,actor模型中的消息传递是异步 的,即消息的发送和接收无需在同一时间进行,发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下,基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。
    – CSP使用显式的Channel用于消息传递,而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下,进程可 以从一个实际上拥有身份标识的channel接收消息,而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。

二、Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel

c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

不带缓冲的Channel兼具通信和同步两种特性,颇受青睐。

三、Channel用作信号(Signal)的场景

1、等待一个事件(Event)

等待一个事件,有时候通过close一个Channel就足够了。例如:

  1. //testwaitevent1.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func main() {
  7. fmt.Println("Begin doing something!")
  8. c := make(chan bool)
  9. go func() {
  10. fmt.Println("Doing something…")
  11. close(c)
  12. }()
  13. <-c
  14. fmt.Println("Done!")
  15. }

这里main goroutine通过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。

关于输出结果:

根据《Go memory model》中关于close channel与recv from channel的order的定义:The closing of a channel happens before a receive that returns a zero value because the channel is closed.

我们可以很容易判断出上面程序的输出结果:

Begin doing something!
Doing something…
Done!

如果将close(c)换成c<-true,则根据《Go memory model》中的定义:A receive from an unbuffered channel happens before the send on that channel completes.
"<-c"要先于"c<-true"完成,但也不影响日志的输出顺序,输出结果仍为上面三行。

2、协同多个Goroutines

同上,close channel还可以用于协同多个Goroutines,比如下面这个例子,我们创建了100个Worker Goroutine,这些Goroutine在被创建出来后都阻塞在"<-start"上,直到我们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。

  1. //testwaitevent2.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func worker(start chan bool, index int) {
  7. <-start
  8. fmt.Println("This is Worker:", index)
  9. }
  10.  
  11. func main() {
  12. start := make(chan bool)
  13. for i := ; i <= ; i++ {
  14. go worker(start, i)
  15. }
  16. close(start)
  17. select {} //deadlock we expected
  18. }

3、Select

【select的基本操作】
select是Go语言特有的操作,使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。

  1. select {
  2. case x := <- somechan:
  3. // … 使用x进行一些操作
  4.  
  5. case y, ok := <- someOtherchan:
  6. // … 使用y进行一些操作,
  7. // 检查ok值判断someOtherchan是否已经关闭
  8.  
  9. case outputChan <- z:
  10. // … z值被成功发送到Channel上时
  11.  
  12. default:
  13. // … 上面case均无法通信时,执行此分支
  14. }

【惯用法:for/select】

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

  1. for {
  2. select {
  3. case x := <- somechan:
  4. // … 使用x进行一些操作
  5.  
  6. case y, ok := <- someOtherchan:
  7. // … 使用y进行一些操作,
  8. // 检查ok值判断someOtherchan是否已经关闭
  9.  
  10. case outputChan <- z:
  11. // … z值被成功发送到Channel上时
  12.  
  13. default:
  14. // … 上面case均无法通信时,执行此分支
  15. }
  16. }

【终结workers】

下面是一个常见的终结sub worker goroutines的方法,每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。

  1. //testterminateworker1.go
  2. package main
  3.  
  4. import (
  5. "fmt"
  6. "time"
  7. )
  8.  
  9. func worker(die chan bool, index int) {
  10. fmt.Println("Begin: This is Worker:", index)
  11. for {
  12. select {
  13. //case xx:
  14. //做事的分支
  15. case <-die:
  16. fmt.Println("Done: This is Worker:", index)
  17. return
  18. }
  19. }
  20. }
  21.  
  22. func main() {
  23. die := make(chan bool)
  24.  
  25. for i := ; i <= ; i++ {
  26. go worker(die, i)
  27. }
  28.  
  29. time.Sleep(time.Second * )
  30. close(die)
  31. select {} //deadlock we expected
  32. }

【终结验证】

有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法:

  1. //testterminateworker2.go
  2. package main
  3.  
  4. import (
  5. "fmt"
  6. //"time"
  7. )
  8.  
  9. func worker(die chan bool) {
  10. fmt.Println("Begin: This is Worker")
  11. for {
  12. select {
  13. //case xx:
  14. //做事的分支
  15. case <-die:
  16. fmt.Println("Done: This is Worker")
  17. die <- true
  18. return
  19. }
  20. }
  21. }
  22.  
  23. func main() {
  24. die := make(chan bool)
  25.  
  26. go worker(die)
  27.  
  28. die <- true
  29. <-die
  30. fmt.Println("Worker goroutine has been terminated")
  31. }

【关闭的Channel永远不会阻塞】

下面演示在一个已经关闭了的channel上读写的结果:

  1. //testoperateonclosedchannel.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func main() {
  7. cb := make(chan bool)
  8. close(cb)
  9. x := <-cb
  10. fmt.Printf("%#v\n", x)
  11.  
  12. x, ok := <-cb
  13. fmt.Printf("%#v %#v\n", x, ok)
  14.  
  15. ci := make(chan int)
  16. close(ci)
  17. y := <-ci
  18. fmt.Printf("%#v\n", y)
  19.  
  20. cb <- true
  21. }

$go run testoperateonclosedchannel.go
false
false false
0
panic: runtime error: send on closed channel

可以看到在一个已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。

【关闭带缓存的channel】

将unbuffered channel换成buffered channel会怎样?我们看下面例子:

  1. //testclosedbufferedchannel.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func main() {
  7. c := make(chan int, )
  8. c <-
  9. c <-
  10. c <-
  11. close(c)
  12. fmt.Printf("%d\n", <-c)
  13. fmt.Printf("%d\n", <-c)
  14. fmt.Printf("%d\n", <-c)
  15. fmt.Printf("%d\n", <-c)
  16.  
  17. c <-
  18. }

$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel

可以看出带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

【range】

Golang中的range常常和channel并肩作战,它被用来从channel中读取所有值。下面是一个简单的实例:

  1. //testrange.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func generator(strings chan string) {
  7. strings <- "Five hour's New York jet lag"
  8. strings <- "and Cayce Pollard wakes in Camden Town"
  9. strings <- "to the dire and ever-decreasing circles"
  10. strings <- "of disrupted circadian rhythm."
  11. close(strings)
  12. }
  13.  
  14. func main() {
  15. strings := make(chan string)
  16. go generator(strings)
  17. for s := range strings {
  18. fmt.Printf("%s\n", s)
  19. }
  20. fmt.Printf("\n")
  21. }

四、隐藏状态

下面通过一个例子来演示一下channel如何用来隐藏状态:

1、例子:唯一的ID服务

  1. //testuniqueid.go
  2. package main
  3.  
  4. import "fmt"
  5.  
  6. func newUniqueIDService() <-chan string {
  7. id := make(chan string)
  8. go func() {
  9. var counter int64 =
  10. for {
  11. id <- fmt.Sprintf("%x", counter)
  12. counter +=
  13. }
  14. }()
  15. return id
  16. }
  17. func main() {
  18. id := newUniqueIDService()
  19. for i := ; i < ; i++ {
  20. fmt.Println(<-id)
  21. }
  22. }

$ go run testuniqueid.go
0
1
2
3
4
5
6
7
8
9

newUniqueIDService通过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需通过channel获得最新id即可。

五、默认情况

我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。

1、select  for non-blocking receive

  1. idle:= make(chan []byte, ) //用一个带缓冲的channel构造一个简单的队列
  2.  
  3. select {
  4. case b = <-idle://尝试从idle队列中读取

  5. default: //队列空,分配一个新的buffer
  6. makes +=
  7. b = make([]byte, size)
  8. }

2、select for non-blocking send

  1. idle:= make(chan []byte, ) //用一个带缓冲的channel构造一个简单的队列
  2.  
  3. select {
  4. case idle <- b: //尝试向队列中插入一个buffer
  5. //…
  6. default: //队列满?
  7.  
  8. }

六、Nil Channels

1、nil channels阻塞

对一个没有初始化的channel进行读写操作都将发生阻塞,例子如下:

  1. package main
  2.  
  3. func main() {
  4. var c chan int
  5. <-c
  6. }
  7.  
  8. $go run testnilchannel.go
  9. fatal error: all goroutines are asleep deadlock!
  10.  
  11. package main
  12.  
  13. func main() {
  14. var c chan int
  15. c <-
  16. }

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

2、nil channel在select中很有用

看下面这个例子:

  1. //testnilchannel_bad.go
  2. package main
  3.  
  4. import "fmt"
  5. import "time"
  6.  
  7. func main() {
  8. var c1, c2 chan int = make(chan int), make(chan int)
  9. go func() {
  10. time.Sleep(time.Second * )
  11. c1 <-
  12. close(c1)
  13. }()
  14.  
  15. go func() {
  16. time.Sleep(time.Second * )
  17. c2 <-
  18. close(c2)
  19. }()
  20.  
  21. for {
  22. select {
  23. case x := <-c1:
  24. fmt.Println(x)
  25. case x := <-c2:
  26. fmt.Println(x)
  27. }
  28. }
  29. fmt.Println("over")
  30. }

我们原本期望程序交替输出5和7两个数字,但实际的输出结果却是:

5
0
0
0
… … 0死循环

再仔细分析代码,原来select每次按case顺序evaluate:
    – 前5s,select一直阻塞;
    – 第5s,c1返回一个5后被close了,“case x := <-c1”这个分支返回,select输出5,并重新select
    – 下一轮select又从“case x := <-c1”这个分支开始evaluate,由于c1被close,按照前面的知识,close的channel不会阻塞,我们会读出这个 channel对应类型的零值,这里就是0;select再次输出0;这时即便c2有值返回,程序也不会走到c2这个分支
    – 依次类推,程序无限循环的输出0

我们利用nil channel来改进这个程序,以实现我们的意图,代码如下:

  1. //testnilchannel.go
  2. package main
  3.  
  4. import "fmt"
  5. import "time"
  6.  
  7. func main() {
  8. var c1, c2 chan int = make(chan int), make(chan int)
  9. go func() {
  10. time.Sleep(time.Second * )
  11. c1 <-
  12. close(c1)
  13. }()
  14.  
  15. go func() {
  16. time.Sleep(time.Second * )
  17. c2 <-
  18. close(c2)
  19. }()
  20.  
  21. for {
  22. select {
  23. case x, ok := <-c1:
  24. if !ok {
  25. c1 = nil
  26. } else {
  27. fmt.Println(x)
  28. }
  29. case x, ok := <-c2:
  30. if !ok {
  31. c2 = nil
  32. } else {
  33. fmt.Println(x)
  34. }
  35. }
  36. if c1 == nil && c2 == nil {
  37. break
  38. }
  39. }
  40. fmt.Println("over")
  41. }

$go run testnilchannel.go
5
7
over

可以看出:通过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。

七、Timers

1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

  1. func worker(start chan bool) {
  2. timeout := time.After( * time.Second)
  3. for {
  4. select {
  5. // … do some stuff
  6. case <- timeout:
  7. return
  8. }
  9. }
  10. }

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

  1. func worker(start chan bool) {
  2. heartbeat := time.Tick( * time.Second)
  3. for {
  4. select {
  5. // … do some stuff
  6. case <- heartbeat:
  7. //… do heartbeat stuff
  8. }
  9. }
  10. }

Golang Channel用法简编的更多相关文章

  1. golang channel 用法转的

    一.Golang并发基础理论 Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论.但就像John Gra ...

  2. Golang channel 用法简介

    channel 是 golang 里相当有趣的一个功能,大部分时候 channel 都是和 goroutine 一起配合使用.本文主要介绍 channel 的一些有趣的用法. 通道(channel), ...

  3. golang channel的使用以及调度原理

    golang channel的使用以及调度原理 为了并发的goroutines之间的通讯,golang使用了管道channel. 可以通过一个goroutines向channel发送数据,然后从另一个 ...

  4. Go基础系列:nil channel用法示例

    Go channel系列: channel入门 为select设置超时时间 nil channel用法示例 双层channel用法示例 指定goroutine的执行顺序 当未为channel分配内存时 ...

  5. Go基础系列:双层channel用法示例

    Go channel系列: channel入门 为select设置超时时间 nil channel用法示例 双层channel用法示例 指定goroutine的执行顺序 双层通道的解释见Go的双层通道 ...

  6. Go语言的管道Channel用法

    本文实例讲述了Go语言的管道Channel用法.分享给大家供大家参考.具体分析如下: channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值. ch <- ...

  7. golang channel关闭后,是否可以读取剩余的数据

    golang channel关闭后,其中剩余的数据,是可以继续读取的. 请看下面的测试例子. 创建一个带有缓冲的channel,向channel中发送数据,然后关闭channel,最后,从channe ...

  8. golang channel原理

    channel介绍 channel一个类型管道,通过它可以在goroutine之间发送和接收消息.它是Golang在语言层面提供的goroutine间的通信方式. 众所周知,Go依赖于称为CSP(Co ...

  9. golang channel 源码剖析

    channel 在 golang 中是一个非常重要的特性,它为我们提供了一个并发模型.对比锁,通过 chan 在多个 goroutine 之间完成数据交互,可以让代码更简洁.更容易实现.更不容易出错. ...

随机推荐

  1. ElasticSearch(二十四)基于scoll技术滚动搜索大量数据

    1.为什么要使用scroll? 如果一次性要查出来比如10万条数据,那么性能会很差,此时一般会采取用scoll滚动查询,一批一批的查,直到所有数据都查询完处理完 2.原理 使用scoll滚动搜索,可以 ...

  2. 关于中国省市的一份js代码

    下面是一份关于中国省市的js代码,搜藏起来,非常有用. var arrCity = [ { name:"请选择", sub:[{name:"请选择"}], ty ...

  3. 11.Django数据库操作(查)

    django.db.models.query.QuerySet1.可迭代2.可切片 官方文档:https://docs.djangoproject.com/en/1.9/ref/models/quer ...

  4. SSAS(SQL Server 分析服务)、***S(SQL Server报表服务)、SSIS(SQL Server集成服务)

    一.数据仓库入门 实验手册 1. 创建数据源  http://jimshu.blog.51cto.com/3171847/13366622. 创建数据源视图 http://jimshu.blog.51 ...

  5. oracle 查询重复数据并且删除, 只保留一条数据重复数据

    最近面试中都遇到了这样一个数据库题: 删除表中的重复数据,有且只保留一条重复数据. 思路: 1)这个题需要用到rowid,首先找到重复数据的rowid,并找出rowid最大或最小值,作为删除的条件: ...

  6. activiti基础--3-----------------------------流程实例

    一.流程实例用到的表: select * from act_ru_execution #正在执行的任务表 select * from act_hi_procinst #流程实例的历史表 select ...

  7. linux shell 中数组使用方法介绍

    linux shell在编程方面比windows 批处理强大太多,不管是在循环.运算.已经数据类型方面都是不能比較的. 以下是个人在使用时候,对它在数组方面一些操作进行的总结. 1.数组定义 [che ...

  8. 导入android sdk samples工程报错"did you mean to use @+id instead of @+android:id?"

    导入“D:\adt-bundle-windows-x86_64-20140702\sdk\samples\android-15”中的工程报错 did you mean to use @+id inst ...

  9. Yii2之事件处理

    通过事件(Event)处理,可以在某个特定时刻执行指定的代码,可以解耦代码,同时也增加了可维护性,通常,事件在客户端软件中比较好理解,比如onClick,onFocus,当点击按钮,获取到焦点时执行指 ...

  10. springboot5

    1.改造购物车系统 1.1.创建购物车的Spring Boot工程 1.1.导入依赖 <project xmlns="http://maven.apache.org/POM/4.0.0 ...