Goroutines and Channels

并发编程,即将一个程序表示为多个自主活动的组合,从来没有像今天这样重要过。Web服务器一次处理数千个客户端的请求。平板电脑和手机应用程序在渲染界面的同时,还在后台执行计算和网络请求。甚至是传统批处理问题----读取数据,计算加工,写入输出---------使用并发性来隐藏I/O操作的延迟,并充分利用现代计算机的多核处理器,这些处理器每年都在增加,但速度却并非也是线性增长。
Go允许两种风格的并发编程。本章介绍Goroutines和Channels,他们支持communicating sequential processes[CSP] (交谈循序程序,又译为通信顺序进程、交换消息的循序程序,一种形式语言,用来描述并发性系统间进行交互的模式),一种并发模型,其中的值会在各个独立的活动/实例(goroutines)之间传递,但变量在很大程度上局限于单个活动/实例(goroutines)上传递。下一章会讲传统的编程模型----共享内存多线程[ shared memory multithreading]。

8.1 Goroutines

在Go中,每个并发执行的活动都被称为goroutine。假设一个程序有两个函数,其中一个做一些计算操作,而另一个则写一些输出,假设两个函数都不调用另一个函数。一个串行的程序可能会先调用一个函数,然后再调用另一个函数;但是在具有两个或很多个goroutines的并发程序中,可以同时对这两个函数的调用。

如果您使用操作系统线程或其他语言中的线程,那么现在您可以假设goroutine与线程类似,这样您将能够编写正确的程序。线程和goroutines之间的区别本质上是定量的,而不是定性的,将在9.8节中描述。

当程序启动时,它唯一的goroutine是调用主函数的那个,所以我们称它为main goroutine。新的goroutines是由go语句创建的。从语法上讲,go语句是一个普通的函数或方法调用,其前缀是关键字go。go语句将在新创建的goroutine中调用函数。go语句本身会立即完成:,不会阻塞调用的goroutine:

f()  // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

在下面的示例中,main goroutine计算第45个斐波那契数。由于它使用了非常低效的递归算法,所以它可以运行相当长的时间,在此期间,我们希望通过显示一个动画图标“转轮”,向用户提供程序仍在运行的视觉指示:

// 执行中的动画图标,即按照指定的周期,在-\|/这四个字符上轮转切换。
func Spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}
//递归的斐波那契数,效率较慢
func Fib(x int) int {
	if x < 2 {
		return x
	}
	return Fib(x-1) + Fib(x-2)
}

func main() {
	go goroutine.Spinner((100 * time.Millisecond))	
	const n = 45
	fibN := goroutine.Fib(n) // 较慢
	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)	
}

几秒钟的动画之后,Fib(45)调用会返回,然后main函数会打印它的结果。

	log: Fibonacci(45) = 1134903170

然后主函数返回。当这种情况发生时,所有goroutines会突然终止,程序退出。除了从主程序返回或直接退出程序之外,一个goroutine没有阻止另一个goroutine的其他程序性方法,但是我们将在后面看到,有一些方法可以与一个goroutine通信,请求它停止自己。
请注意,程序是如何表示为两个独立自治的活动的组合的:spinning和Fibonacci计算。每一个都是作为单独的函数编写的,但是它们都是并发的。

8.2 Example: Concurrent Clock Server

网络编程是使用并发大显身手的领域,因为服务器通常需要一次处理来自其客户端的许多连接,每个客户端本质上独立于其他客户端。在本节中,我们将介绍net包,它提供了用于构建通过TCP、UDP或Unix域套接字进行通信的网络客户端和服务器程序的组件。我们从第一章开始就一直使用的net/http包是在net包的函数之上构建的。
我们的第一个示例是一个顺序时钟服务器,它每秒向客户机写入一次当前时间:

func main() {
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}

	for  {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
			continue
		}
		handleConn(conn)
	}
}
func handleConn(conn net.Conn){
	defer conn.Close()
	for {
		_, err := io.WriteString(conn, time.Now().Format("15:04:05\n"))
		if err != nil {
			return // e.g., client disconnected
		}
		time.Sleep(1 * time.Second)
	}
}

Listen函数创建了一个net.Listener,该对象侦听网络端口上传入的连接,在我们的这个用例作用,在localhost:8000上监听的TCP协议。该net.Listener对象的Accept函数会阻塞,直到有一个连接请求进入,然后会返回一个net.Conn对象,用于表示连接。
handleConn函数处理一个完整的客户端连接。在循环中,它向客户端写入当前时间,即time. now()。因为net.Conn满足/实现了io.Writer接口,我们可以直接向它做写操作。当写操作失败时,循环结束,很可能是因为客户端断开连接,这时handleConn使用延迟调用(defer)来关闭连接并返回,继续等待下一个连接请求。
time.Time.Format方法通过一个自定义的示例,提供了格式化日期和时间信息的方式。它的实参是一个模板,指示如何格式化一个参考时间,具体地说,Mon Jan 2 03:04:05PM 2006 UTC-0700。参考时间有八个组件组成。可以以任意的形式组合前面的这个模板;出现在模板中部分,会作为时间格式化的参考。这里我们只使用时间中的小时、分钟和秒。time包为许多标准时间格式(如time.RFC1123)定义了模板。相同的机制也适用于解析时间的time.Parse 。

为了连接到服务端,我们需要一个客户端程序,例如nc(“netcat”),他是一个操作网络连接的标准实用程序:

$ go build gopl.io/ch8/clock1
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C

客户端会将服务器每秒发送的时间展示出来,知道我们通过Ctrl+C终端客户端连接。如果没有安装netcat,那么可以使用telnet或者下面这个简单的Go版本的netcat,它使用net.Dial来间接到TCP服务器。
该程序从连接中读取数据并将其写入标准输出,直到出现文件结束条件或错误。mustCopy函数是本节中几个示例中使用的实用程序。让我们在不同的终端上同时运行两个客户端,一个显示在左边上,另一个显示在右边:

$ go build gopl.io/ch8/netcat1
$ ./netcat1
13:58:54  							$ ./netcat1
13:58:55
13:58:56
^C
									13:58:57
									13:58:58
									13:58:59
									^C
$ killall clock1

killall是一个Unix实用程序,他会根据制定的名字,终止某个进程。
第二个客户端必须等待到第一个客户端机完成,因为服务器是连续的;它一次只处理一个客户。只需做一个小的更改就可以使服务器并发:将go关键字添加到对handleConn的调用中,使每个调用都可以在自己的goroutine中运行。

8.3. Example: Concurrent Echo Server

时钟服务器为每一个连接启用一个 goroutine。在本节,我们着手构建一个echo服务器,他会为每一个连接使用多个goroutine。大多数情况下echo服务器仅仅会将其所读取到信息原样返回,就像下面的handleConn所做的:

func handleConn(c net.Conn) {
	io.Copy(c, c) // NOTE: ignoring errors
	c.Close()
}

一个更有趣的echo服务器可能会模拟真实echo中的“回音”,一开始要用大写的(“HELLO!”)来表示声音很大,然后经过一小段延迟后,则是一个温和的(“Hello!”),然后是一个全小写的(“hello!”)表示声音正在渐渐变小,最后消失掉,就像这个版本的handleConn:

func echo(c net.Conn, shout string, delay time.Duration) {
	fmt.Fprintln(c, "\t", strings.ToUpper(shout))
	time.Sleep(delay)
	fmt.Fprintln(c, "\t", shout)
	time.Sleep(delay)
	fmt.Fprintln(c, "\t", strings.ToLower(shout))
}
func handleConn(c net.Conn) {
	input := bufio.NewScanner(c)
	for input.Scan() {
		echo(c, input.Text(), 1*time.Second)
	}
	// NOTE: ignoring potential errors from input.Err()
	c.Close()
}

我们需要升级我们的客户端程序,以便它向服务器发送终端输入,同时将服务器的响应复制到标准输出,这提供了另一个使用并发的机会:

func main() {
	conn, err := net.Dial("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	go mustCopy(os.Stdout, conn)
	mustCopy(conn, os.Stdin)
}

当main goroutine读取标准输入并将其发送到服务器时,第二个goroutine读取并打印服务器的响应。当main goroutine遇到终止输入,例如,当用户在终端键入Control-D(^ D)(或相当于Control-Z Microsoft Windows),程序将会停止,即使其他goroutine仍有工作要做。

在下面的会话中,客户端的输入是左对齐的,服务器的响应是缩进的。客户对echo服务器大叫了三次:

$ go build gopl.io/ch8/reverb1
$ ./reverb1 &
$ go build gopl.io/ch8/netcat2
$ ./netcat2
Hello?
	HELLO?
	Hello?
	hello?
Is there anybody there?
	IS THERE ANYBODY THERE?
Yooo-hooo!
	Is there anybody there?
	is there anybody there?
	YOOO-HOOO!
	Yooo-hooo!
	yooo-hooo!
^D
$ killall reverb1

请注意,客户发出的第三声喊叫要等到第二声后才处理,这是不现实的。真正的回声应该由三个独立的叫声组成的。为了模拟它,我们需要更多的goroutines。同样,我们需要做的就是添加go关键字,这一次我们加在对echo函数调用的地方:

func handleConn(c net.Conn) {
	input := bufio.NewScanner(c)
	for input.Scan() {
		go echo(c, input.Text(), 1*time.Second)
	}
	// NOTE: ignoring potential errors from input.Err()
	c.Close()
}

go后跟着的函数的参数,会在go语句自身执行时被求值;因此input.Text()实在main goroutine中计算。
现在的回声是同步且重叠的:

$ go build gopl.io/ch8/reverb2
$ ./reverb2 &
$ ./netcat2
Is there anybody there?
	IS THERE ANYBODY THERE?
Yooo-hooo!
	Is there anybody there?
	YOOO-HOOO!
	is there anybody there?
	Yooo-hooo!
	yooo-hooo!
^D
$ killall reverb2

要使服务器可以利用并发性,不仅要并发的处理来自多个客户机的连接,甚至在单个连接中也可以用到并发性,就像我们的Echo Server例子中插入两个go关键字。
但是在添加这些关键字时,我们必须仔细考虑并发地调用Conn方法是否是安全的,对于绝大多数类型来说,确实是不安全的。我们将在下一章讨论并发安全性的关键概念。

8.4 Channels

如果说goroutine是Go并发程序中的活动,那么channel则可以说是他们之间的连接。channel是一种通信机制,它使得一个goroutine可以向其他goroutine发送值。每个channel都是一个管道,用于处理特定类型的值,称为channel的元素类型【element type】。元素类型为int的channel的类型写为:chan int
我们可以使用内置的make函数来创建一个channel:

	ch := make(chan int) // ch has type 'chan int'

和map类型类似,channel也是一个对make创建的底层数据结构的引用。当我拷贝一个channel,或者将channel作为函数的参数传递时,我们只是拷贝了这个引用而已,所以调用者和被调用者所引用的事同一个channel对象。与其他的引用类型一样,channel的零值也是nil。
具有想用类型的channel,可以使用==来比较。如果两个channel所引用的是相同的channel数据结构,那么比较的结果就是true;channel亦可以与nil比较。

channel有两个主要的操作:send和receive,统称为通信[communications]。发送语句将一个值从一个goroutine通过channel传输到另一个执行相应接收表达式的goroutine。这两个操作都是使用<-操作符来写的。在发送语句中,<-分隔了channel和值。在接收表达式中,<-写在channel之前。未使用接收结果的接收表达式也是有效的语句。

ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch  // a receive statement; result is discarded

channel还支持第三个操作:Close,即设置一个标志,指示不再在此通道上发送任何值;如果随后又向其发送值,那么将会引起恐慌。在Close了的channel上执行接收操作,那么依然可以接收到之前已经成功发送的值,直到没有更多的值剩下为止;任何接收操作然后会立即完成,并生成channel的元素类型的零值。
要想关闭一个channel,我们只需要使用内置的close函数即可:

	close(ch)

简单的通过调用make函数创建的channel是一个无缓存的channel。但是make函数还可以指定第二个参数,该参数用于指定channel的容量【capacity】。如果该容量的值并不是0,那么make函数会创建一个缓冲的channel.

	ch = make(chan int) // unbuffered channel
	ch = make(chan int, 0) // unbuffered channel
	ch = make(chan int, 3) // buffered channel with capacity 3

8.4.1 无缓冲的channel

在无缓冲channel上执行的发送操作将阻塞执行发送操作的goroutine,直到另一个goroutine在同一通道上执行相应的接收操作,此时值被传输,并且两个goroutine都可能继续。相反,如果先尝试先执行接收操作,则会阻塞执行接收操作的goroutine,直到另一个goroutine在同一channel上执行发送操作。

在无缓冲的channel上的通信导致执行发送和接收操作的goroutines同步。因此,无缓冲的channel也被称为sunchronous channel(同步通道)。在无缓冲的channel上发送值时,先是接收者的goroutine接收到数据,然后才唤醒发送者的goroutine。

在讨论并发性时,当我们说x happen before y时,我们不仅仅指x在y之前发生;我们的意思是,它肯定会这样做,并且它的所有先前的效果,例如变量的更新,都是已经完成了的,并且您可以安全的依赖它们。

当我们说x既不是happen before y,也不是happen after y,那么我们可以说x是与y并发执行的。这并不意味着x和y一定是同时存在的,只是我们不能假设它们的顺序。在下一章中,我们将看到,在程序执行期间,有必要对某些事件进行排序,以避免两个goroutines同时访问同一个变量时出现的问题。

8.3节中的客户端程序其main 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
}

当用户关闭标准输入流时,mustCopy返回,main goroutine调用conn.Close(),关闭网络的读和写两个方向的连接。关闭网络连接的写方向的连接,会导致服务器接收到一个文件结束【end-of-file】的信号。关闭读方向的连接,取导致后台goroutine调用io.Copy函数,返回一个“从关闭连接读取【read from closed connection】”错误,这就是为什么我们删除了错误日志记录;练习8.3中将提出了一个更好的解决方案。(注意go语句调用了一个函数字面量,这是一个常见的构造形式)。
在返回之前,后台的goroutine会打印一个日志信息,然后向done对应的channel上发送一个值。main goroutine在退出之前会阻塞等待,直到从channel中接收到了一个值为止。程序一版在退出之前会打印一个“done”消息出来。
通过channel发送的消息有两个重要方面。首先每条信息都有值,但有时通信的事实和它发生的时刻同样重要。当我们希望强调这一点时,我们会称其为消息事件【message event】。当事件并没有附加信息,也就是说,它的唯一目的是同步【synchronization】,我们会通过使用struct{}{}空结构体作为channel的元素类型,来强调这一点。虽然我们也可以使用bool或者int类型实现同样的功能,而且 done <- 1也远比done <- struct{}{}更简短。

8.4.2 Pipelines

Channel可以用于将-goroutines连接在一起,这样前一个Channel的输出作为下一个Channel的输入。这种串联的形式的Channel也被称为Popeline。下面的程序由两个channel将三个goroutine串联起来,如图8-1 所示:

在这里插入图片描述
图8-1 一个三阶段的pipeline
第一个goroutine,即图中所示的counter,生成0,1,2,…等的数值,并将它们通过channel发送到第二个goroutine,即图中的squarer,该goroutine会接收每一个,并取平方,然后将计算后得劲结果通过另一个channel发往第三个goroutine,即图中的printer,该goroutine会接收平方后的值,并打印。

func main() {
	naturals := make(chan int)
	squares := make(chan int)
	// Counter
	go func() {
		for x := 0; ; x++ {
			naturals <- x
		}
	}()
	// Squarer
	go func() {
		for {
		x := <-naturals
			squares <- x * x
		}
	}()
	// Printer (in main goroutine)
	for {
		fmt.Println(<-squares)
	}
}

如您所料,该程序打印了无数的平方数0,1,4,9,等等。像这样的串联channel的管道(pipeline)可以在需要长时间运行的服务器程序中找到,其中的channel被用于在包含无限循环的goroutines之间,无休止的通信。但是,如果我们只想通过管道发送有限数量的值呢?

如果发送方知道永远不会有其他值通过channel发送出去的话,那么让接受者也能及时的知道没有多余的值需要接收,这将是很有用的,因为这样他们就可以停止等待。这是通过内置的close函数关闭channel来实现:

close(naturals)

当一个channel被关闭后,任何在该channel上进行的进一步的发送操作都会引起恐慌。在关闭的通道被耗尽之后,也就是说,在接收端接收到最后一个发送的元素之后,所有后续的接收操作将继续进行,且不会阻塞,而是会立即产生一个零值。关闭上面的naturals对应的channel将导致squarer的循环在无休止的接收到零值而继续流转,并且将这些零值发送到printer。

//Counter
func Generate(ch chan(int) ){
	for x := 0; x < 100000 ; x++ {
		ch <- x
	}
	close(ch)
}
//当我们关闭了该channel,那么下游的Squarer会持续不断受到int的零值,且无阻塞

没有办法来直接测试一个channel是否被关闭,但是接收操作有一个变体操作形式:它会接收两个结果,一个是channel所发送的元素值,外加一个bool值,一般称为ok,当true时表示接收成功,当false时表示channel已经被关闭且里面已经没有值可以再接收了。使用这个特性,我们可以修改squarer的循环,以便在naturals对应的channel耗尽时停止,并依次关闭squares对应的channel:

// Squarer
go func() {
	for {
		x, ok := <-naturals
		if !ok {
			break // channel was closed and drained
		}
		squares <- x * x
	}
	close(squares)
}()

由于上面的语法很笨拙,但是这种模式又很常见,因此Go语言允许我们使用range循环来遍历channel。这是一种更方便的语法,用于接收在通道上发送的所有值,并在最后一个循环之后终止循环。

在下面的管道(pipeline)中,当Counter对应的goroutine在迭代100个元素之后完成循环时,它关闭变量naturals所对应的channel,导致Squarer对应的goroutine完成循环并关闭变量square所对应的channel。(在更复杂的程序中,我们可以使用defer来关闭channel)。最后,main 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)
	}
}

其实并不需要在你结束时关闭每一个channel。只有当需要告诉接收者的goroutine,所有的数据都已经全部发送完毕时。无论一个channel是否被关闭,当他没有被引用时,就会被Go的垃圾回收机制处理(不要将此与打开文件的关闭操作混淆。当您用完每一个文件时,调用Close方法是很重要的。)

试图关闭已经关闭的channel会引起恐慌,关闭nil channel也是如此。关闭channel还有另外一种广播机制的用途,在8.9中讲解。

8.4.3 单向通道类型

随着程序的增长,我们很自然的需要将大的功能分解成小的部分。我们前面的示例使用了三个goroutines,通过两个通道进行通信,这是main的局部变量。我么可以很自然的将程序分为三个功能部分:

	func counter(out chan int)
	func squarer(out, in chan int)
	func printer(in chan int)

处于管道(pipeline)中间的squarer函数可以接受两个参数,输入channel和输出channel。两者都有相同的类型,但它们的预期用途是相反的:in只用于接收,out只用于发送。in和out的变量名已经表达了这种意图,但仍然不能阻止squarer向in所对应的channel发送消息,或者从out所对应的channel读取消息。

这种安排是很典型的。当一个channel被提供来作为一个函数的参数时,它几乎总是带有这样的意图:它只用于发送或接收。

为了文档化这种意图并防止误用,Go类型系统提供单向通道类型,只公开发送和接收操作中的一个或另一个。类型chan<- int, 是一个send-only[仅发送]int类型的值得channel,允许发送但不允许接收。相反,类型<- chan int则是一个receive only[仅接收]int类型的值得channel,只允许接收但不发送(<- 的箭头相对于chan关键字的位置是一个助记符)。在编译时检测到违反该规范的行为。

由于close操作符会断言不会再在该channel上发生发送行为,因此只有负责发送消息的goroutine能够调用它,因此试图关闭receive-only[仅接收]的channel是编译时错误。

func counter(out chan<- int) {
	for x := 0; x < 100; x++ {
		out <- x
	}
	close(out)
}
func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}
func printer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}
func main() {
	naturals := make(chan int)
	squares := make(chan int)
	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
}

调用counter(naturals)会隐式地将变量naturals所对应的chan int类型的channel转换为该函数的参数的类型chan<- int。调用printer(squares)则会执行类似的转换,转换该函数的参数的类型<- chan int。任何任务都允许从双向通道转换为单向通道类型。但是,这种转换没有逆向的:一旦有了单向类型的值,比如chan<- int,你是无法从其中获得引用相同通道数据结构的chan int类型的值。

8.4.4 缓冲通道【buffered channel】

缓冲通道【buffered channel】有一个元素队列。队列的最大大小在创建队列时由容量参数决定。下面的语句创建一个能够容纳三个字符串值的缓冲通道【buffered channel】。图8.2是ch及其所指向的通道的图形表示。

	ch = make(chan string, 3)

在这里插入图片描述

如果在缓冲通道【buffered channel】上执行的发送操作的话,那么将在队列的后面插入一个元素,而接收操作将从前面删除一个元素。如果缓冲通道【buffered channel】已满,发送操作将阻塞执行该操作所在的goroutine,直到另一个接收端的goroutine的消费掉队列中的值,提供可用空间。相反,如果缓冲通道【buffered channel】为空,接收操作将阻塞,直到另一个负责执行发送操作的goroutine发送一个值为止。
我们可以向该缓冲通道【buffered channel】发送3个值,而不会被阻塞掉:

ch <- "A"
ch <- "B"
ch <- "C"

此时,该缓冲通道【buffered channel】已经满了(图8-3),如果再执行第四个插入语句会被阻塞。
在这里插入图片描述

如果我们接收一个值:

fmt.Println(<-ch) // "A"

此时的缓冲通道【buffered channel】既不是满的也不是空的(图8.4),因此发送操作或接收操作都可以在不阻塞的情况下进行。通过这种方式,缓冲通道【buffered channel】的缓冲区可以解耦发送端的goroutines和接收端的goroutines。
在这里插入图片描述

在不太可能的情况下,程序需要知道通道的缓冲区容量,它可以通过调用内置的cap函数获得:

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

当应用到缓冲通道【buffered channel】时,内置的len函数返回当前已缓冲的元素的数量。由于在并发程序中,一旦检索到这些信息,这些信息就可能已经失效了,因此其值是有限的,但在故障诊断或性能优化时,它可能非常有用。

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

当我们在此缓冲通道【buffered channel】执行两次接收操作,那么此时缓冲通道【buffered channel】的缓冲区变为空的,如果在此基础上执行第四次接收操作,那么会阻塞;

fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"

在本例中,发送和接收操作都是在相同的goroutine上执行,但是在实际的程序中,它们通常在不同的goroutine上执行。因为语法简单,新手可能会尝试在一个单独的goroutine中使用缓冲通道[buffered channel]作为队列,但这是一个错误的用法。channel与goroutine调度紧密相连,如果没有另一个goroutine从channel接收值,那么发送者——甚至可能是整个程序——就有可能永远阻塞的风险。如果您只需要一个简单的队列,那么就使用切片来实现即可。

下面的示例显示了一个使用缓冲通道【buffered channel】的应用程序。它向三个镜像【 mirrors】发出并行请求,这三个镜像【 mirrors】是等效的,但在地理上是分布式的服务器。它们将响应发送到一个缓冲通道【buffered channel】上,然后我们只接收并返回第一个返回的响应,即最快到达的响应。因此,mirroredQuery会在其他两个较慢的服务器响应返回之前,就结束方法并返回结果了。(顺便说一下,多个goroutines并发地将值发送到相同的channel,这是很正常的,在本例中就是这样,或者从相同的channel接收值。)

func mirroredQuery() string {
	responses := make(chan string, 3)
	go func() { responses <- request("asia.gopl.io") }()
	go func() { responses <- request("europe.gopl.io") }()
	go func() { responses <- request("americas.gopl.io") }()
	return <-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }

如果我们使用的是Unbufferd Channel,那两个较慢的goroutine就会被卡在一个永远不会有goroutine接收的channel上。这种情况,被称为goroutine泄漏,将是一个bug。与垃圾变量不同的是,泄漏的goroutines不会自动收集,因此,重要的是要确保goroutines在不再需要的时候自动终止。

Buffered Channel和Unbufferd Channel之间的选择,以及Buffered Channel容量的选择,这都可能影响程序的正确性。Unbufferd Channel提供了更强的同步保证,因为每个发送操作都与其相应的接收操作相互同步;而有了Buffered Channel,这些操作就解耦了。另外,当我们知道将在Channel上发送的值的数量的上限时,通常会创建一个这样大小的Buffered Channel,并在接收第一个值之前执行所有的发送。如果未能分配足够的缓冲区容量,这将导致程序死锁。
如果每个厨师之间有一块蛋糕的空间,厨师可以在那里放一块做好的蛋糕,然后立即开始做下一块;这类似于容量为1的缓冲通道【buffered channel】。只要厨师们的平均工作速度大致相同,大部分的交接工作就会进行得很快,从而消除了他们各自速度带来的暂时差异。更大的缓冲间可以允许有更大的间隔,可以在不影响流水线的情况下消除更大的瞬时波动,比如,一个厨师休息一会儿,然后又急急忙忙赶着赶上去。

另一方面,如果流水线的前一个阶段始终比下一个阶段快,那么他们之间就会把大部分时间都花在缓冲这上面。相反,如果后期更快,缓冲区通常为空。在这种情况下,缓冲区没有提供任何好处。

流水线的隐喻对于channels和goroutines来说是非常有用的。例如,如果流水线的第二阶段更复杂,一个厨师可能无法跟上第一个厨师的供应或满足第三个厨师的需求。为了解决这个问题,我们可以雇佣另一个厨师来帮助第二个厨师,完成同样的任务,但独立的工作。这类似于创建了一个新的goroutine,然后再在相同的channel上通信。

我们没有地方再去展示它,但是gopl.io/ch8/cake包中模拟了这个蛋糕店。

8.5 并行循环

在本节中,我们将探索一些常见的并发模式,用于并行执行循环的所有迭代。我们将考虑从一组全尺寸图片中生成拇指指甲大小的图片的问题。gopl.io/ch8/thumbnail包提供了一个ImageFile函数,可以缩放一张单独的图片。我们不会展示它的实现,但是它可以从 gopl.io下载

package thumbnail
// ImageFile reads an image from infile and writes
// a thumbnail-size version of it in the same directory.
// It returns the generated file name, e.g., "foo.thumb.jpg".
func ImageFile(infile string) (string, error)

下面的程序循环遍历图片文件名列表,并为每个文件名生成缩略图:

// makeThumbnails makes thumbnails of the specified files.
func makeThumbnails(filenames []string) {
	for _, f := range filenames {
		if _, err := thumbnail.ImageFile(f); err != nil {
			log.Println(err)
		}
	}
}

显然,我们处理文件的顺序无关紧要,因为每个缩放操作都独立于所有其他操作。像这样完全由完全相互独立子问题所组成的问题被描述成高度平行【 embarrassingly parallel】。高度平行【 embarrassingly parallel】的并行问题是最容易同时实现的,并且可以享受与并行度成正比的性能。

让我们并行执行所有这些操作,从而将文件I/O延迟隐藏掉,并利用多核CPU的计算能力来缩放图片。我们对并发版本的第一次尝试只是添加了一个go关键字。我们暂时忽略错误,以后再处理它们。

// NOTE: incorrect!
func makeThumbnails2(filenames []string) {
	for _, f := range filenames {
		go thumbnail.ImageFile(f) // NOTE: ignoring errors
	}
}

这一版运行得实在太快了,事实上,即使文件名slice中只包含一个元素,这一版程序所花费的时间还是要比原始版本的要少。那么如果程序中没有并行,为什么并发版本还是要比串行的快呢?答案是,makeThumbnails在它还没有完成之前就已经返回了。它启动了所有的goroutine,每一个文件名对应一个goroutine,但是我们没有等待他们执行完。

// makeThumbnails3 makes thumbnails of the specified files in parallel.
func makeThumbnails3(filenames []string) {
	ch := make(chan struct{})
	for _, f := range filenames {
		go func(f string) {
			thumbnail.ImageFile(f) // NOTE: ignoring errors
			ch <- struct{}{}
		}(f)
	}
	// Wait for goroutines to complete.
	for range filenames {
		<-ch
	}
}

注意,我们将f的值作为一个显式的参数传递给字面量函数,而不是使用for循环中声明的变量f:

for _, f := range filenames {
	go func() {
		thumbnail.ImageFile(f) // NOTE: incorrect!
	// ...
	}()
}

回想一下第5.6.1节中描述的匿名函数内的循环变量捕获问题。上面的代码中,循环变量f由所有匿名函数值共享,并通过连续的循环迭代进行更新。当新的goroutine开始执行时这字面量函数时,for循环可能更新了f,并开始另一次迭代或者整个结束,所以,当这些goroutine读取f的值时,他们所看到的可能是切片的最后一个元素的值,通过在字面量函数上添加显式的参数,我们确保在执行go语句时使用当前的f值。

如果我们想从每个worker goroutine返回值到main goroutine呢?如果调用thumbnail.ImageFile创建文件失败,他会返回一个错误。下一个版本的makeThumbnails则会返回它从任何缩放操作中接收到的第一个错误:

// makeThumbnails4 makes thumbnails for the specified files in parallel.
// It returns an error if any step failed.
func makeThumbnails4(filenames []string) error {
	errors := make(chan error)
	for _, f := range filenames {
		go func(f string) {
			_, err := thumbnail.ImageFile(f)
			errors <- err
		}(f)
	}
	for range filenames {
		if err := <-errors; err != nil {
			return err // NOTE: incorrect: goroutine leak!
		}
	}
	return nil
}

这个函数有一个微妙的错误。当遇到第一个非nil的error时,它会将error返回给调用者,但是没有留下一个goroutine去耗尽error所对应的channel。当每个剩余的worker goroutine试图再向那个channel上发送值时,它将被永远阻塞,并且永远不会终止。这种情况下就导致了goroutine泄漏(§8.4.4),可能会导致整个程序卡住或耗尽内存。
最简单的解决方案是使用一个具有足够容量的缓冲通道【buffered channel】,使得当worker goroutine向其发送消息时不会被阻塞掉(另一种解决方案是创建另一个goroutine来耗尽error所对应的channel,而主goroutine立即返回第一个error)。

// makeThumbnails5 makes thumbnails for the specified files in parallel.
// It returns the generated file names in an arbitrary order,
// or an error if any step failed.
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
	type item struct {
		thumbfile string
		err  error
	}
	ch := make(chan item, len(filenames))
	for _, f := range filenames {
		go func(f string) {
			var it item
			it.thumbfile, it.err = thumbnail.ImageFile(f)
			ch <- it
		}(f)
	}
	for range filenames {
		it := <-ch
		if it.err != nil {
			return nil, it.err
		}
		thumbfiles = append(thumbfiles, it.thumbfile)
	}
	return thumbfiles, nil
}

下面是makeThumbnails的最终版本,它返回了新文件占用的总字节数。但是,与以前的版本不同,它接收的文件名不是切片,而是字符串channel,因此我们无法预测循环迭代的次数。

要知道最后一个goroutine什么时候结束(可能它并不是最后一个开始的),我们需要在每个goroutine开始之前对计数器递增,并在每个goroutine结束时对计数器递减。这就需要一种特殊的计数器,一种可以在多个goroutines上安全操纵的计数器,它提供了一种等待计数器变为零的方法。这种计数器类型称为sync.WaitGroup。下面的代码展示了如何使用它:

// makeThumbnails6 makes thumbnails for each file received from the channel.
// It returns the number of bytes occupied by the files it creates.
func makeThumbnails6(filenames <-chan string) int64 {
	sizes := make(chan int64)
	var wg sync.WaitGroup // number of working goroutines
	for f := range filenames {
		wg.Add(1)
	// worker
		go func(f string) {
			defer wg.Done()
			thumb, err := thumbnail.ImageFile(f)
			if err != nil {
				log.Println(err)
				return
			}
		info, _ := os.Stat(thumb) // OK to ignore error
		sizes <- info.Size()
		}(f)
	}
	// closer goroutine
	go func() {
		wg.Wait()
		close(sizes)
	}()
	var total int64
	for size := range sizes {
		total += size
	}
	return total
}

注意Add和Done方法中的不对称。递增计数器的Add操作必须在worker goroutine启动之前调用,而不是在它内部;否则,我们就无法确定Add是在close goroutine调用Wait之前发生的。另外,Add取一个参数,但Done却没有这么做;它等同于 Add(-1)。我们使用defer来确保即使在错误情况下,计数器也会减少。上面代码结构是在不知道迭代次数时并行循环的常见惯用模式。

sizes所对应的channel会将每一个文件的大小返回给main goroutine,而main goroutine会使用range循环来迭代,并计算总文件大小。注意观察我们是如何创建的这个closer goroutine,该goroutine会等待所有的worker goroutine结束后,才会关闭sizes所对应的channel。这里有两个操作,即wait和close,他们必须与slice上的循环并发执行。考虑其他选项:如果在循环之前将wait操作放置在main goroutine中,那么它将永远不会结束;如果在循环之后放置,它将无法到达,因为没有任何东西关闭通道,循环将永远不会结束。

图8.5演示了makeThumbnails6函数中的事件序列。垂直线代表goroutines。细的部分表示睡眠,粗的部分表示活动中。斜箭头表示使一个goroutine与另一个goroutine同步的事件。时间向下流动。请注意,main goroutine如何将大部分时间花费在range循环中的睡眠上,睡眠是为了等待worker发送一个值或closer去关闭通道。
在这里插入图片描述

8.6 Example: 并发Web爬虫

在5.6节中,我们制作了一个简单的web爬虫程序,以广度优先算法抓取整个Web的链接图。在本节中,我们将使其并发,以便对crawl的独立调用能够利用web中可用的I/O并行性。

8.7 基于select的多路复用

下面的程序为火箭发射倒计时。time.Tick函数返回一个channel,它在该channel上周期性地发送事件,就像一个节拍器似的。每个事件的值是一个时间戳,不过有意思的是其传送方式。

现在,让我们添加这样一个功能,通过在倒计时期间按下返回键来中止发射。首先,我们启动一个goroutine,它尝试从标准输入读取单个字节,如果成功读取到,则向名为abort的channel上发送一个值.

现在火箭发射的倒计时循环的每次迭代需要等待一个这两个channel其中的一个返回事件了:如果一切都很好(在美国宇航局“nominal”术语)的话,那么我们会从tick对应的channel收到事件,而当出现“异常”时,我们则会从abort所对应的channel上收到事件。我们无法做到从每一个channel中接收消息,如果我们这样做的话,假设第一个channel中还没有事件发送过来,那么程序就会被阻塞,这样我们就无法及时的收到第二个channel中发送过来的事件。为此,我们需要一个select语句:

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

上面显示了select语句的一般形式。与switch语句一样,它有许多case和可选的default。每个case指定一个通信(在某个channel上的发生的发送或接收操作)和一个相关的语句块。接收表达式可以如第一个case中那样,单独出现,或可以像第二个case中那样,出现在短变量声明中;第二种形式允许您引用接收到的值。

select会一直等待,直到某个case中的通信准备好继续进行。然后执行该通信并执行该case中相关语句;这时候其他的通信不会被执行。一个没有case的select, 即select{},将永远等待。
然我们回到火箭发射的程序。time.After函数会立即返回一个channel,并启动一个新的goroutine,该goroutine在指定时间后在该通道上发送一个值。下面代码中的select会一直等待,直到两个事件中的一个到达为止,无论这个事件是一个中断事件,还是一个指示已经过10秒了的事件。如果10秒事件过了,但是却并未收到中断事件,那么就会点火火箭发射。

下面的例子更加微妙。ch所对应的channel,其缓冲区大小为1,它时而为空,时而为满。因此只有一种情况可以继续进行,要么是i为偶数时发送,要么是i为奇数时接收。它总是打印0 2 4 6 8

ch := make(chan int, 1)
for i := 0; i < 10; i++ {
	select {
	case x := <-ch:
		fmt.Println(x) // "0" "2" "4" "6" "8"
	case ch <- i:
	}
}

如果准备了多种case,select会随机的选择其中的一种case,这确保了每个channel都有同等的机会被选中。增加前一个示例的缓冲区大小会使得其输出变得不确定,因为当缓冲区既不满也不空时,select语句会象征性地抛出一个硬币来决定选择哪个case。
让我们让火箭发射程序可以打印倒计时。下面的select语句会导致循环的每次迭代都会等待1秒,除非有中断事件发出。

func main() {

	abort := make(chan struct{})
	tick := time.Tick(1 * time.Second)
	go func() {
		os.Stdin.Read(make([]byte,1))
		abort <- struct{}{}
	}()
	for countDown := 10; countDown > 0 ; countDown-- {
		select {
		case <- tick:
		case <- abort:
			fmt.Println("Launch aborted!")
			return
		}
	}
	fmt.Printf("launch~~")
}

time.Tick函数的行为就好像它创建了一个在循环中调用了time.Sleep的goroutine,每次被唤醒时发送一个事件。当上面的函数因为接收到中断事件而结束时,它停止从tick所对应的channel上接收事件,但ticker goroutine仍然存在,并徒劳地将事件发送到这个永远不会有接收操作发生的channel上-----goroutine泄漏(§8.4.4)。
Tick函数很方便,但只有在应用程序的整个生命周期中都需要节拍器时才合适。否则,我们应该使用以下模式:

ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate

有时我们希望在channel上发送或接收事件时,并避免因channel未准备好而导致的阻塞------即实现非阻塞通信。select语句也可以做到这一点。select有一个default,它指定当其他通信都不能立即进行时该做什么。
下面的select语句从abort对应的channel接收一个值(如果要接收一个);否则它什么也不做。这是一个非阻塞的接收操作;重复这样做称为轮询通道。

select {
case <-abort:
	fmt.Printf("Launch aborted!\n")
	return
default:
// do nothing
}

channel的零值是nil。毫无疑问,nil通道有时是有用的。
因为在nil chanel上执行发送和接收操作会永远阻塞,所以select语句中channel为nil的case永远不会被选中。这使我们可以使用nil启用或禁用与处理超时或取消、响应其他输入事件或发出输出等特性对应的case。我们将在下一节中看到一个示例。

8.8. Example: Concurrent Directory Traversal

在本节中,我们将构建一个程序,报告在命令行中指定的一个或多个目录的磁盘使用情况,类似于Unix的du命令。它的大部分工作是由下面的walkDir函数完成的,该函数使用dirents助手函数枚举列出变量dir所对应的目录下的条目。

// walkDir recursively walks the file tree rooted at dir
// and sends the size of each found file on fileSizes.
func walkDir(dir string, fileSizes chan<- int64) {
	for _, entry := range dirents(dir) {
		if entry.IsDir() {
			subdir := filepath.Join(dir, entry.Name())
			walkDir(subdir, fileSizes)
		} else {
			fileSizes <- entry.Size()
		}
	}
}
	// dirents returns the entries of directory dir.
func dirents(dir string) []os.FileInfo {
	entries, err := ioutil.ReadDir(dir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "du1: %v\n", err)
		return nil
	}
	return entries
}

ioutil.ReadDir函数返回一个切片,该切片内的元素的类型为os.FileInfo----- os.Stat 函数的返回值也是一个os.FileInfo类型的值。对于每个子目录,walkDir递归地调用自己,对于每个文件,walkDir则会在fileSizes所对应的channel上发送一条消息。消息是文件的字节大小(以字节为单位)。

下面的main函数中,使用了两个goroutine。后台的goroutine为命令行中指定的每个目录调用walkDir,最后关闭fileSizes对应的channel。main goroutine计算它从channel中接收到的所有的文件大小的总和,最后输出总大小值。

// The du1 command computes the disk usage of the files in a directory.
package main
import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
)
func main() {
	// Determine the initial directories.
	flag.Parse()
	roots := flag.Args()
	if len(roots) == 0 {
		roots = []string{"."}
	}
	// Traverse the file tree.
	fileSizes := make(chan int64)
	go func() {
		for _, root := range roots {
			walkDir(root, fileSizes)
		}
		close(fileSizes)
	}()
	// Print the results.
	var nfiles, nbytes int64
	for size := range fileSizes {
		nfiles++
		nbytes += size
	}
	printDiskUsage(nfiles, nbytes)
}
func printDiskUsage(nfiles, nbytes int64) {
	fmt.Printf("%d files %.1f GB\n", nfiles, float64(nbytes)/1e9)
}

该程序在打印结果之前暂停了很长一段时间:

$ go build gopl.io/ch8/du1
$ ./du1 $HOME /usr /bin /etc
213201 files 62.7 GB

如果这个程序能让我们知道它的处理进度,那就更好了。然而,简单地将printDiskUsage调用移到循环中就会导致数千行输出。

下面是du的一种变体形式,它会定期的打印到目前为止所统计好的总字节数,但只有在指定了-v标志时才会这样做,因为不是所有用户都想看到进度消息。循环root的后台goroutine保持不变。现在,main goroutine使用一个r每500ms生成一个事件的时钟滴答器,并使用一个select语句等待一个文件大小消息(在这种情况下,它更新总数),或者一个时钟滴答事件(在这种情况下,它打印到目前为止所统计好的总字节数)。如果没有指定-v标志,那么tick所对应的channel仍然为nil,那么它在select中的case实际上是被禁用的。

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"
)
var verbose = flag.Bool("v", false, "show verbose progress messages")
func main() {

	flag.Parse()

	roots := flag.Args()
	if len(roots) == 0 {
		roots = []string{"."}
	}
	//发送文件大小的channel
	fileSizes := make(chan int64)


	go func() {
		for _,dir := range roots {
			newWalkDir(dir,fileSizes)
		}
		close(fileSizes)
	}()



	//时钟滴答器
	var tick <-chan time.Time
	if *verbose {
		//如果-v被使用,则才初始化该channel
		tick = time.Tick(500 * time.Millisecond)
	}
	var nfiles, nbytes int64
loop:
	for {
		select {
		case size,ok := <- fileSizes:
			if !ok {
				//没有数据了
				break loop
			}
			nfiles++
			nbytes = nbytes + size
		case <- tick:
			printDiskUsage(nfiles, nbytes )
		}
	}
	printDiskUsage(nfiles, nbytes) // final totals

}

func newWalkDir(dir string,channel  chan <- int64){

	for _,info := range dirents(dir) {
		if info.IsDir() {
			subdir := filepath.Join(dir, info.Name())
			newWalkDir(subdir,channel)
		} else {
			channel <- info.Size()
		}
	}
}

func dirents(dir string) []os.FileInfo {
	infos, err := ioutil.ReadDir(dir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "du1: %v\n", err)
		return nil
	}
	return infos
}

由于程序不再使用range循环,select语句中的第一个case必须显式地使用元组返回值,测试fileSizes对应的channel是否关闭了。如果channel已经关闭,程序就会跳出循环。这种带有标签的break语句同时跳出select和for循环;一个光杆的break只会跳出select,导致for循环开始下一次迭代。
该程序现在悠闲的给我们打印更新流:

$ go build gopl.io/ch8/du2
$ ./du2 -v $HOME /usr /bin /etc
28608 files 8.3 GB
54147 files 10.3 GB
93591 files 15.1 GB
127169 files 52.9 GB
175931 files 62.2 GB
213201 files 62.7 GB

然而,它仍然需要很长时间才能完成。通过利用磁盘系统中的并行性,没有理由不能并发地执行所有对walkDir的调用。下面是du的第三个版本,它为每个对walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup(§8.5)来对仍然活跃的walkDir调用进行计数,还使用另一个closer goroutine在计数器递减为0时关闭fileSizes对应的channel.

func main() {
	// ...determine roots...
	// Traverse each root of the file tree in parallel.
	fileSizes := make(chan int64)
	var n sync.WaitGroup
	for _, root := range roots {
		n.Add(1)
		go walkDir(root, &n, fileSizes)
	}
	go func() {
		n.Wait()
		close(fileSizes)
	}()
	// ...select loop...
}
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
	defer n.Done()
	for _, entry := range dirents(dir) {
		if entry.IsDir() {
			n.Add(1)
			subdir := filepath.Join(dir, entry.Name())
			go walkDir(subdir, n, fileSizes)
		} else {
			fileSizes <- entry.Size()
		}
	}
}

由于这个程序在其峰值时创建了成千上万个goroutines,我们不得不改变dirents函数,使它使用计数信号量来防止它一次打开太多的文件,就像我们在8.6节中对web爬虫程序所做的:

// sema is a counting semaphore for limiting concurrency in dirents.
var sema = make(chan struct{}, 20)
// dirents returns the entries of directory dir.
func dirents(dir string) []os.FileInfo {
	sema <- struct{}{} // acquire token
	defer func() { <-sema }() // release token
	// ...

这个版本的运行速度比上一个版本快好几倍,尽管其速度还是受到运行环境和机器配置的影响。

8.9. Cancellation

有时,我们需要指示goroutine停止它正在做的事情,例如,一个正在执行计算的Web服务,但是他的客户端可能已经断开了连接。

一个goroutine无法直接终止另一个goroutine,因为这会使所有共享变量处于未定义状态。在火箭发射程序中(§8.7),我们想名为abort的channel上发送了一个值,这负责倒计时的goroutine将其解释为一个请求停止本身的消息。但如果我们需要取消两个goroutines,或者任意数量的goroutines呢?

第一种可能的方案,是在abort所对应的channel上发送与待取消的goroutines数量相同的的事件。但是,如果有一些goroutines已经终止退出了它们自己,那么我们发送的事件数量多于goroutine,这样我们的发送操作就会被阻塞(因为没有足够的goroutine来接收这些事件)。另一方面,如果这些goroutine又产生了其他的goroutine,那么我们发送的事件数量又太少了,必然有一些goroutine接收不到退出的消息。一般来说,很难知道在任何时候有多少个goroutines正在为我们工作。此外,当goroutine从abort所对应的channel接收到值时,它会消费掉这个值,这样其他的goroutine就看不到它了。对于取消,我们需要的是一个可靠的机制,以便通过一个channel广播事件,以便许多goroutines都可以看到广播时看到这条消息,并在事件处理完成之后,能够知道这条消息已经发生过。

回想一下,在关闭Channel并耗尽所有已发送的值之后,随后的接收操作将无阻塞的立即执行,产生零值。我们可以不利用这一机制来创建一个广播机制:不再Channel上发送任何值,而是直接关闭掉它。

我们只需要作出一点改变,就可以向上一节的du程序添加Cancellation(即goroutine退出)功能。首先我们创建一个负责Cancellation(即goroutine退出)的channel,我们不会向这个channel发送任何值

首先,我们创建了一个负责Cancellation(即goroutine退出)的Channel,在这个通道上没有发送任何值,但是其所在的闭包要表明程序需要退出。然后我们定义一个工具函数,cancelled,这个函数在它被调用时会检查或者轮询它的cancellation状态。

var done = make(chan struct{})
func cancelled() bool {
	select {
	case <-done:
	return true
	default:
	return false
	}
}

然后,我们会创建一个goroutine,,它将会从标准输入读取,而这个标准输入通常连接到的事终端。当我们从输入端读取到任何的值(比如用户按下了取消按钮),那么这个goroutine会通过关闭done所对应的Channel,来广播Cancellation消息。

// Cancel traversal when input is detected.
go func() {
	os.Stdin.Read(make([]byte, 1)) // read a single byte
	close(done)
}()

现在我们让我们的goroutine来对这条Cancellation消息响应。在main goutine,我们向select语句中添加了第三种case,从done所对应的Channel上接收消息。如果这种case被选中,函数将会返回,但是在返回之前,它必须先清空fileSizes通道,丢弃所有值,直到Channel被关闭。它这样做是为了确保对walkDir的任何主动调用都可以运行到完成,而不会被卡在向fileSizes所对应的Channel发送文件字节大小的消息的过程上。

for {
	select {
	case <-done:
		// Drain fileSizes to allow existing goroutines to finish.
		for range fileSizes {
		// Do nothing.
		}
		return
	case size, ok := <-fileSizes:
		// ...
	}
}

执行walkDir的gorouitine会从一启动开始,就轮询cancellation状态,如果该状态被设置了,那么就直接返回,而不会做任何其他事。

func walkDir(dir string, n *sync.WaitGroup, fileSizes chan <- int64) {
	defer n.Done()
	if cancelled() {
		return
	}
	for _, entry := range dirents(dir) {
		// ...
	}
}

在walkDir循环中再次轮询cancellation状态,以避免在cancellation事件发出之后又创建了新的goroutines,这可能是有益的。取消goroutine本身有一定代价的;要想获得更快的响应,通常需要对程序逻辑进行更深入的更改。确保在cancellation事件之后不会发生代价昂贵的操作,可能需要更新代码中的许多位置,但通常大多数好处都可以通过在一些重要位置进行cancellation检查获得。

通过对这个程序进行简要的性能分析表明,瓶颈是在dirents函数的获取信号量令牌中。下面的select使这个操作可以取消,并将程序的cancellation延迟从几百毫秒减少到几十毫秒:

func dirents(dir string) []os.FileInfo {
	select {
	case sema <- struct{}{}: // acquire token
	case <-done:
		return nil // cancelled
	}
	defer func() { <-sema }() // release token
		// ...read directory...
}

现在,当cancellation发生时,所有的后台goroutines会迅速停止,主函数返回。当然,当main返回时,程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源。在测试期间,我们可以使用一个简便的技巧:如果在cancellation发生的情况下,我们不从main返回,而是执行一个调用以引起恐慌,那么runtime将转储程序中每个goroutine的堆栈。如果只剩下main goroutine的堆栈信息,那么代表它就会自己清理干净。但如果其他的goroutine仍然存在,那么可能这些goroutine没有被正确地取消,或者它们已经被取消了,但是取消需要时间;所有稍微调查一下可能是值得的。恐慌转储通常包含足够的信息来帮助我们验证情况。

8.10 聊天服务

我们将使用一个聊天服务器来完成这一章,该服务器允许多个用户互相广播文本消息。在这个程序中有四种goroutine。我们又一个执行main函数的goroutine,还有一个执行broadcaster的goroutines实例,每个客户端连接都会有一个执行handleConn函数的goroutine和一个执行clientWriter函数的goroutine。广播很好地说明了如何使用select,因为它必须响应三种不同类型的消息。

如下所示,执行main函数的goroutine的工作是侦听并接收来自客户端传入的网络连接。对于每一个,它都会创建一个新执行handleConn函数的goroutine,就像我们在本章开头看到的并发echo服务器一样。

func main() {
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	go broadcaster()
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn)
	}
}

接下来是broadcaster函数。它的本地变量clients 记录了当前已连接的客户端集。我们对每一个客户端所记录的信息,只记录了其输出消息Channel的标识,后面会详细介绍

type client chan<- string // an outgoing message channel
var (
	entering = make(chan client)
	leaving = make(chan client)
	messages = make(chan string) // all incoming client messages
)
func broadcaster() {
	clients := make(map[client]bool) // all connected clients
	for {
		select {
		case msg := <-messages:
			// Broadcast incoming message to all
			// clients' outgoing message channels.
			for cli := range clients {
				cli <- msg
			}
		case cli := <-entering:
			clients[cli] = true
		case cli := <-leaving:
			delete(clients, cli)
			close(cli)
		}
	}
}

该broadcaster函数会监听全局的entering和leaving变量所对应的Channel,来获得客户端接入和离开的通知。当该函数接收到这其中一种事件,他会更新clients集,如果这个这个事件是表示客户端离开的,他会关闭向该客户端输出消息的channel。该broadcaster函数还会监听全局的messages变量对应的Channel,所有的客户端都会向这个Channel发送消息。当broadcaster函数接收到这种时事件,他会将该消息广播给所有已连接的客户端(模拟聊天)。

现在我们来看一下每一个客户端的goroutine,handleConn函数创建了一个新的向它的客户端输出消息的Channel,并向entering对应的Channel上发送消息,宣布该客户端上线了。然后他会读取客户端发来的每一行文本输入,并将所读取到的信息通过全局的message变量所对应的Channel发送出去,注意,他会为这每一条消息,附带上发送者的前缀标识以表明身份。一旦不再需要从客户端读取数据,handleConn就会通过变量leaving对应的Channel宣布客户端离开,并关闭连接。

func handleConn(conn net.Conn) {
	ch := make(chan string) // outgoing client messages
	go clientWriter(conn, ch)
	who := conn.RemoteAddr().String()
	ch <- "You are " + who
	messages <- who + " has arrived"
	entering <- ch
	input := bufio.NewScanner(conn)
	for input.Scan() {
		messages <- who + ": " + input.Text()
	}
	// NOTE: ignoring potential errors from input.Err()
	leaving <- ch
	messages <- who + " has left"
	conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
	for msg := range ch {
		fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
	}
}

此外,handleConn为每一个客户端创建了一个运行clientWriter函数的goroutine,用于接收广播到客户端的传出消息通道的消息,并将其写入客户端的网络连接。当broadcaster程序在收到leaving变量对应的Channel的通知关闭Channel时,客户端写入器循环终止。

下面的日志显示了服务器在同一台计算机上的两个客户端在不同的窗口中运行,并使用netcat聊天:

$ ./chat &
$ ./netcat3
You are 127.0.0.1:64208 					$ ./netcat3
127.0.0.1:64211 has arrived 				You are 127.0.0.1:64211
Hi!
127.0.0.1:64208: Hi! 							127.0.0.1:64208: Hi!
Hi yourself.
127.0.0.1:64211:Hi yourself. 				127.0.0.1:64211: Hi yourself.
^C
															127.0.0.1:64208 has left
$ ./netcat3
You are 127.0.0.1:64216 					127.0.0.1:64216 has arrived
Welcome.
127.0.0.1:64211: Welcome. 				127.0.0.1:64211: Welcome.
															^C
127.0.0.1:64211 has left

虽然当n个进行客户聊天会话时,这个程序同时运行2n+2个goroutine用于通信,然而,它不需要显式锁操作(§9.2)。map类型的clients仅仅可以被运行broadcaster的goroutine,因此不能并发访问它。多个goroutine共享的唯一变量是Chanel和net.Conn 实例,而两者都是并发安全的。在下一章中,我们将讨论约束、并发安全性以及跨goroutines共享变量的含义。

猜你喜欢

转载自blog.csdn.net/qq_31179577/article/details/83384313