Go语言圣经 .7.goroutine和通道

goroutine和通道

go有两种并发编程的风格

  • goroutine
  • 通道(channel)

goroutine

在go里,每一个并发执行的活动称为goroutine。

当一个程序启动时,只有一个goroutine来调用main函数,称它为主goroutine。

新的goroutine通过go语句进行创建。语法上,一个go语句是在普通的函数或者方法调用前家上go关键字前缀。go语句使函数在一个新创建的goroutine中调用。go语句本身的执行立即完成:

f()   // 调用f(); 等待返回
go f()// 新建一个调用f()的goroutine,不用等待

示例:并发时钟服务器

示例:并发回声服务器

通道(channel)

如果说goroutine是go程序并发的执行体,通道就是他们之间的连接。通道是可以让一个goroutine发送特定值到另一个goroutine的通信机制。每一个通道是一个具体类型的导管,叫作通道的元素类型。一个有int类型元素的通道写作chan int使用内置的make函数来创建一个通道:

ch := make(chan int) // ch的类型是chan int

像map一样,通道是要给使用make创建的数据结构的引用。

通道的零值是nil

通道支持close操作,用于关闭channel。关闭后对于channel的发送操作都将导致panic。对于一个已经close的通道。接收操作依然可以接受到之前已经成功发送的数据;如果没有数据了,可以获得一个零值

通道的两个主要操作:

  • 发送(send):通道在<-符号的左边,值在右边
  • 接受(receive):<-放在通道操作数据前面
  • 关闭(close): close(ch)

两者统称为通信

ch <- x  //发送语句
x = <-ch // 赋值语句中的接收表达式
<-ch // 接收语句,丢弃结果
ch = make(chan int)		// 无缓冲通道
ch = make(chan int, 0) // 无缓冲通道
ch = make(chan int, 3) // 容量为3的缓冲通道

无缓冲通道

无缓冲通道上的发送操作将会阻塞,知道另一个goroutine在对应的通道上上执行接受操作。

使用无缓冲通道进行的通信导致发送和接受goroutine同步化,因此,无缓冲通道也称作同步通道。

管道

通道可以用来连接goroutine,这样一个的输出是另一个的输入。这个叫作管道(pipeline)。

单向通道类型

类型chan<- int是一个只能发送的通道

类型<-chan int是一个只能接收的通道

通道缓冲

通道的容量,可以调用内置函数cap获取

fmt.Println(cap(ch))  // "2"

并行循环

示例:并发的web爬虫

使用select多路复用

select {
case <- ch1:
// ...
case x: <-ch2:
// ...
case ch3 <- y:
// ...
default:
// ...
}

和switch语句一样,它有一系列的情况和一个可选的默认分支。每一个情况指定一次通信和关联一段代码块。

对于没有对应情况的select, select{}将永远等待

如果多个情况同时满足,select随机选择一个。

示例:并发目录遍历

取消

示例:聊天服务器

练习

练习 8.1

修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可 以同时与多个clock服务器通信,从多服务器中读取时间,并且在一个表格中一次显示所有服务传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器 可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:

$ TZ=US/Eastern ./clock2 -port 8010 & $ TZ=Asia/Tokyo ./clock2 -port 8020 & 
$ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030

练习 8.2

实现一个并发FTP服务器。服务器应该解析客户端来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。

练习 8.3

在netcat3例子中,conn虽然是一个interface类型的值,但是其底层真实类型是 *net.TCPConn ,代表一个TCP连接。一个TCP连接有读和写两个部分,可以使用 CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码,只关闭网络连 接中写的部分,这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传 回的数据。(要在reverb2服务器也完成同样的功能是比较困难的;参考练习 8.4。)

练习 8.4

修改reverb2服务器,在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时,关闭TCP连接的写入,像练习8.3中一样。验证一下你的修改版 netcat3客户端会一直等待所有的并发“喊叫”完成,即使是在标准输入流已经关闭的情况下。

练习 8.5

使用一个已有的CPU绑定的顺序程序,比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序,并将他们的主循环改为并发形式,使用channel来进行通 信。在多核计算机上这个程序得到了多少速度上的改进?使用多少个goroutine是最合适的 呢?

练习 8.6

为并发爬虫增加深度限制。也就是说,如果用户设置了depth=3,那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。

练习 8.7

完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org结尾,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理, 使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。

练习 8.8

使用select来改造8.3节中的echo服务器,为其增加超时,这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。

练习 8.9

编写一个du工具,每隔一段时间将root目录下的目录大小计算并显示出来。

练习 8.10

HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。(提示:http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之,设置它的Cancel字段,然后用http.DefaultClient.Do(req)来进行这个http请求。)

练习 8.11

紧接着8.4.4中的mirroredQuery流程,实现一个并发请求url的fetch的变种。当第一个请求返回时,直接取消其它的请求。

练习 8.12

使broadcaster能够将arrival事件通知当前所有的客户端。为了达成这个目的,你需要有一个客户端的集合,并且在entering和leaving的channel中记录客户端的名字。

练习 8.13

使聊天服务器能够断开空闲的客户端连接,比如最近五分钟之后没有发送任何消息的那些客户端。提示:可以在其它goroutine中调用conn.Close()来解除Read调用,就像input.Scanner()所做的那样。

练习 8.14

修改聊天服务器的网络协议这样每一个客户端就可以在entering时可以提供它们的名字。将消息前缀由之前的网络地址改为这个名字。

练习 8.15

如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息,而不是等待这个客户端一直到其准备好写。或者为每一个客户 端的消息发出channel建立缓冲区,这样大部分的消息便不会被丢掉;broadcaster应该用一个 非阻塞的send向这个channel中发消息。