第八章-goroutine和通道
goroutine:在go里,每一个并发执行的活动成为goroutine。语法上,一个go语句实在普通的函数或者方法调用前加上go关键字。
go f() //新建一个f()的goroutine,不用等待
通道:如果说goroutine是Go程序并发的执行体,通道就是他们之间的连接。通道是可以让一个叫goroutine发送特定值到另一个goroutine的通信机制。每一个通道是一个具体类型的导管,叫做通道的元素类型。一个有int类型元素的通道写为chan int。
使用make函数来创建一个通道:
ch := make(chan int) // ch的类型是 ‘chan int’
像map一样,通道是一个使用make创建的数据结构的引用。当复制或者作为参数传递到一个函数事,复制的是引用,这样调用者和被调用者都引用同一份数据结构。和其他引用类型一样,通道的零值是nil。
同种类型的通道可以使用==进行比较。当二者都是同一通道数据的引用时,比较值为true。通道也可以和nil进行比较。
通道主要有两个操作:发送和接收,两者称为通信
ch <- x //发送语句
x = <-ch //赋值语句中的接收表达式
<-ch //接收语句,丢弃结果
通道支持第三个操作:关闭,他设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了;关闭后的发送操作将导致宕机。
在一个已经关闭的通道上进行接收,将获取所有已经发送的值,直到通道为空;这时任何接收操作会立刻完成,同时获取到一个通道元素类型对应的零值。
close(ch)
无缓冲通道:
无缓冲通道上的发送操作将会阻塞,知道另一个goroutine在对应的通道上执行接收操作,这是值传送完成,两goroutine都可以继续执行。相反,如果接受操作先执行,接收方goroutine将阻塞,直到另一个goroutine在同一个通道上发送一个值。
使用无缓冲通道进行的通信导致发送和接收goroutine同步化。当一个值在无缓冲通道上传递时,接收值后发送方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) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done // wait for background goroutine to finish
}
每一个消息有一个值,但有时候通信本身以及通信发生的时间也很重要。当我们强调这方面的时候,把消息叫做事件。当事件没有携带额外的信息时,它单纯的目的使进行同步。
管道:通道可以用来连接goroutine,这样一个得输出是另一个得输入。
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
并发的Web爬虫:程序得并行度太高了,无限制得并行通常不是一个好得注意,因为系统中总有限制因素,例如CUP得核数,对于磁盘I/O操作磁头和磁盘得个数,下载流所使用得网络带宽等。解决办法是根据资源可用情况限制并发得个数,以匹配合适得并行度。
var tokens = make(chan struct{}, 20)
func crawl(url string) []string {
fmt.Println(url)
tokens <- struct{}{} // acquire a token
list, err := links.Extract(url)
<-tokens // release the token
if err != nil {
log.Print(err)
}
return list
}
在执行之前调用tokens <- struct{}{}获取一个名额,执行结束后调用<-tokens释放名额。
select多路复用:像switch语句一样,有一系列的情况和一个可选的默认分支。每一个情况指定一次通信(在一些通道上进行发送或接收操作)和关联的一段代码。
接收操作可以出现在它本身上,也可以用引用所接受的值。
select一直等待,直到一次通信来告知有一些情况可以执行,它执行这次通信,其他的通信将不会发生。没有对应情况的select,将永远等待。
func main() {
ch :=make(chan int,20)
for i:=0;i<10;i++{
select {
case x := <- ch:
fmt.Println(x)
case ch<-i:
}
}
}
上面,如果多个情况同时满足,select随机选择一个,因为当缓冲区既不空也不满的情况,select相当于在掷硬币选择。
select {
case <- abort:
fmt.Printf("Launch aborted!\n")
return
default:
//不执行任何操作
}
非阻塞通信。通道的零值是nil。在nil通道上发送和接收将永远阻塞,对于select语句中的情况,如果其通道是nil,他将永远不会被选择。称为通道的轮询。对于select,如果其通道是nil,他将永远不会被选择。