channel 是 golang 里至关有趣的一个功能,大部分时候 channel 都是和 goroutine 一块儿配合使用。本文主要介绍 channel 的一些有趣的用法。golang
通道(channel)
,像是通道(管道),能够经过它们发送类型化的数据在协程之间通讯,能够避开全部内存共享致使的坑;通道的通讯方式保证了同步性。数据经过通道:同一时间只有一个协程能够访问数据:因此不会出现数据竞争,设计如此。数据的归属(能够读写数据的能力)被传递。shell
通道其实是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)结构的因此能够保证发送给他们的元素的顺序(有些人知道,通道能够比做 Unix shells 中的双向管道(tw-way pipe))。通道也是引用类型,因此咱们使用 make()
函数来给它分配内存。缓存
2、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将被置为falselua
默认状况下,通讯是同步且无缓冲的:在有接受者接收数据以前,发送不会结束。能够想象一个无缓冲的通道在没有空间来保存数据的时候:必需要一个接收者准备好接收通道的数据而后发送者能够直接把数据发送给接收者。因此通道的发送/接收操做在对方准备好以前是阻塞的:spa
1)对于同一个通道,发送操做(协程或者函数中的),在接收者准备好以前是阻塞的:若是ch中的数据无人接收,就没法再给通道传入其余数据:新的输入没法在通道非空的状况下传入。因此发送操做会等待 ch 再次变为可用状态:就是通道值被接收时(能够传入变量)。.net
2)对于同一个通道,接收操做是阻塞的(协程或函数中的),直到发送者可用:若是通道中没有数据,接收者就阻塞了。设计
3、Channel用做信号(Signal)的场景(信号量)code
一、等待一个事件(Event)
等待一个事件。例如:
package main
import "fmt"
func main() {
fmt.Println("Begin doing something!")
c := make(chan bool)
go func() {
fmt.Println("Doing something…")
close(c)
}()
<-c
fmt.Println("Done!")
}
这里main goroutine经过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine经过close channel促发这一事件。固然也能够经过向Channel写入一个bool值的方式来做为事件通知。main goroutine在channel c上没有任何数据可读的状况下会阻塞等待。
二、协同多个Goroutines
同上,close channel还能够用于协同多个Goroutines,好比下面这个例子,咱们建立了100个Worker Goroutine,这些Goroutine在被建立出来后都阻塞在"<-start"上,直到咱们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。
//testwaitevent2.go
package main
import "fmt"
func worker(start chan bool, index int) {
<-start
fmt.Println("This is Worker:", index)
}
func main() {
start := make(chan bool)
for i := 1; i <= 100; i++ {
go worker(start, i)
}
close(start)
select {} //deadlock we expected
}
三、Select
从不一样的并发执行的协程中获取值能够经过关键字select
来完成,它和switch
控制语句很是类似(章节5.3)也被称做通讯开关;它的行为像是“你准备好了吗”的轮询机制;select
监听进入通道的数据,也能够是用通道发送值的时候。
select
作的就是:选择处理列出的多个通讯状况中的一个。
default
语句,它就会执行:default
永远是可运行的(这就是准备好了,能够执行)。下面是select的基本操做。
select {
case x := <- somechan:
// … 使用x进行一些操做
case y, ok := <- someOtherchan:
// … 使用y进行一些操做,
// 检查ok值判断someOtherchan是否已经关闭
case outputChan <- z:
// … z值被成功发送到Channel上时
default:
// … 上面case均没法通讯时,执行此分支
}
我想这里John Graham-Cumming主要是想告诉咱们select的default分支的实践用法。
一、select for non-blocking receive
idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列
select {
case b = <-idle:
//尝试从idle队列中读取
…
default: //队列空,分配一个新的buffer
makes += 1
b = make([]byte, size)
}
二、select for non-blocking send
idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列
select {
case idle <- b: //尝试向队列中插入一个buffer
//…
default: //队列满?
}
【惯用法:for/select】
咱们在使用select时不多只是对其进行一次evaluation,咱们经常将其与for {}结合在一块儿使用,并选择适当时机从for{}中退出。
for {
select {
case x := <- somechan:
// … 使用x进行一些操做
case y, ok := <- someOtherchan:
// … 使用y进行一些操做,
// 检查ok值判断someOtherchan是否已经关闭
case outputChan <- z:
// … z值被成功发送到Channel上时
default:
// … 上面case均没法通讯时,执行此分支
}
}
【终结workers】
下面是一个常见的终结sub worker goroutines的方法,每一个worker goroutine经过select监视一个die channel来及时获取main goroutine的退出通知。
//testterminateworker1.go
package main
import (
"fmt"
"time"
)
func worker(die chan bool, index int) {
fmt.Println("Begin: This is Worker:", index)
for {
select {
//case xx:
//作事的分支
case <-die:
fmt.Println("Done: This is Worker:", index)
return
}
}
}
func main() {
die := make(chan bool)
for i := 1; i <= 100; i++ {
go worker(die, i)
}
time.Sleep(time.Second * 5)
close(die)
select {} //deadlock we expected
}
【终结验证】
有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法:
//testterminateworker2.go
package main
import (
"fmt"
//"time"
)
func worker(die chan bool) {
fmt.Println("Begin: This is Worker")
for {
select {
//case xx:
//作事的分支
case <-die:
fmt.Println("Done: This is Worker")
die <- true
return
}
}
}
func main() {
die := make(chan bool)
go worker(die)
die <- true
<-die
fmt.Println("Worker goroutine has been terminated")
}
【关闭的Channel永远不会阻塞】
通道能够被显式的关闭;尽管它们和文件不一样:没必要每次都关闭。只有在当须要告诉接收者不会再提供新的值的时候,才须要关闭通道。只有发送者须要关闭通道,接收者永远不会须要。
下面演示在一个已经关闭了的channel上读写的结果:
//testoperateonclosedchannel.go
package main
import "fmt"
func main() {
cb := make(chan bool)
close(cb)
x := <-cb
fmt.Printf("%#v\n", x)
x, ok := <-cb
fmt.Printf("%#v %#v\n", x, ok)
ci := make(chan int)
close(ci)
y := <-ci
fmt.Printf("%#v\n", y)
cb <- true
}
$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会怎样?咱们看下面例子:
//testclosedbufferedchannel.go
package main
import "fmt"
func main() {
c := make(chan int, 3)
c <- 15
c <- 34
c <- 65
close(c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
c <- 1
}
$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel
能够看出带缓冲的channel略有不一样。尽管已经close了,但咱们依旧能够从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操做也会触发panic。
4、隐藏状态(自增加ID生成器)
下面经过一个例子来演示一下channel如何用来隐藏状态:
一、例子:惟一的ID服务
//testuniqueid.go
package main
import "fmt"
func newUniqueIDService() <-chan string {
id := make(chan string)
go func() {
var counter int64 = 0
for {
id <- fmt.Sprintf("%x", counter)
counter += 1
}
}()
return id
}
func main() {
id := newUniqueIDService()
for i := 0; i < 10; i++ {
fmt.Println(<-id)
}
}
newUniqueIDService经过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需经过channel得到最新id便可。
5、默认状况(最多见的方式:生产者/消费者)
生产者产生一些数据将其放入 channel;而后消费者按照顺序,一个一个的从 channel 中取出这些数据进行处理。这是最多见的 channel 的使用方式。当 channel 的缓冲用尽时,生产者必须等待(阻塞)。换句话说,如果 channel 中没有数据,消费者就必须等待了。
生产者
func producer(c chan int64, max int) { defer close(c) for i:= 0; i < max; i ++ { c <- time.Now().Unix() } }
生产者生成“max”个 int64 的数字,而且将其放入 channel “c” 中。须要注意的是,这里用 defer 在函数推出的时候关闭了 channel。
消费者
func consumer(c chan int64) { var v int64 ok := true for ok { if v, ok = <-c; ok { fmt.Println(v) } } }
从 channel 中一个一个的读取 int64 的数字,而后将其打印在屏幕上。当 channel 被关闭后,变量“ok”将被设置为“false”。
6、Nil Channels
一、nil channels阻塞
对一个没有初始化的channel进行读写操做都将发生阻塞,例子以下:
package main
func main() {
var c chan int
<-c
}
$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!
package main
func main() {
var c chan int
c <- 1
}
$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!
二、nil channel在select中颇有用
看下面这个例子:
//testnilchannel_bad.go
package main
import "fmt"
import "time"
func main() {
var c1, c2 chan int = make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
for {
select {
case x := <-c1:
fmt.Println(x)
case x := <-c2:
fmt.Println(x)
}
}
fmt.Println("over")
}
咱们本来指望程序交替输出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来改进这个程序,以实现咱们的意图,代码以下:
//testnilchannel.go
package main
import "fmt"
import "time"
func main() {
var c1, c2 chan int = make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
for {
select {
case x, ok := <-c1:
if !ok {
c1 = nil
} else {
fmt.Println(x)
}
case x, ok := <-c2:
if !ok {
c2 = nil
} else {
fmt.Println(x)
}
}
if c1 == nil && c2 == nil {
break
}
}
fmt.Println("over")
}
$go run testnilchannel.go
5
7
over
能够看出:经过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。
7、Timers(超时定时器)
一、超时机制Timeout
带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:
func worker(start chan bool) {
timeout := time.After(30 * time.Second)
for {
select {
// … do some stuff
case <- timeout:
return
}
}
}
二、心跳HeartBeart
与timeout实现相似,下面是一个简单的心跳select实现:
func worker(start chan bool) {
heartbeat := time.Tick(30 * time.Second)
for {
select {
// … do some stuff
case <- heartbeat:
//… do heartbeat stuff
}
}
}
参考自:
http://blog.csdn.net/erlib/article/details/44097291
http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/