1、Golang并发基础理论程序员
Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并很少,毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人能够从这里下载到CSP论文的最新版本。golang
维基百科中概要罗列了CSP模型与另一种并发模型Actor模型的区别:缓存
Actor模型广义上讲与CSP模型很类似。但两种模型就提供的原语而言,又有一些根本上的不一样之处:
– CSP模型处理过程是匿名的,而Actor模型中的Actor则具备身份标识。
– CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。相反,actor模型中的消息传递是异步 的,即消息的发送和接收无需在同一时间进行,发送方能够在接收方准备好接收消息前将消息发送出去。这两种方案能够认为是彼此对偶的。在某种意义下,基于交 会点的系统能够经过构造带缓冲的通讯的方式来模拟异步消息系统。而异步系统能够经过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通讯 方式。
– CSP使用显式的Channel用于消息传递,而Actor模型则将消息发送给命名的目的Actor。这两种方法能够被认为是对偶的。某种意义下,进程可 以从一个实际上拥有身份标识的channel接收消息,而经过将actors构形成类Channel的行为模式也能够打破actors之间的名字耦合。并发
2、Go Channel基本操做语法app
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兼具通讯和同步两种特性,颇受青睐。lua
3、Channel用做信号(Signal)的场景spa
一、等待一个事件(Event)设计
等待一个事件,有时候经过close一个Channel就足够了。例如:
//testwaitevent1.go 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上没有任何数据可读的状况下会阻塞等待。
关于输出结果:
根据《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"完成,但也不影响日志的输出顺序,输出结果仍为上面三行。
二、协同多个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的基本操做】
select是Go语言特有的操做,使用select咱们能够同时在多个channel上进行发送/接收操做。下面是select的基本操做。
select {
case x := <- somechan:
// … 使用x进行一些操做
case y, ok := <- someOtherchan:
// … 使用y进行一些操做,
// 检查ok值判断someOtherchan是否已经关闭
case outputChan <- z:
// … z值被成功发送到Channel上时
default:
// … 上面case均没法通讯时,执行此分支
}
【惯用法: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。
【range】
Golang中的range经常和channel并肩做战,它被用来从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
4、隐藏状态
下面经过一个例子来演示一下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) } } $ go run testuniqueid.go 0 1 2 3 4 5 6 7 8 9
newUniqueIDService经过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需经过channel得到最新id便可。
5、默认状况
我想这里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: //队列满?
}
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 } } }