pull/10/head
Xargin 2016-10-05 13:42:01 +08:00
parent 5fd1e1d48f
commit f2df739842
6 changed files with 19 additions and 19 deletions

View File

@ -29,11 +29,11 @@ func main() {
}
```
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络链接中的写方向的链接将导致server程序收到一个文件end-of-file结束的信号。关闭网络链接中读方向的链接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这Go语言中启动goroutine常用的形式。
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-file结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这Go语言中启动goroutine常用的形式。
在后台goroutine返回之前它先打印一个日志信息然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出“done”消息。
基于channels发送消息有两个重要方面。首先每个消息都有一个值但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时我们将它称为**消息事件**。有些消息事件并不携带额外的信息它仅仅是用作两个goroutine之间的同步这时候我们可以用`struct{}`空结构体作为channels元素的类型虽然也可以使用bool或int类型实现同样的功能`done <- 1``done <- struct{}{}`
**练习 8.3** 在netcat3例子中conn虽然是一个interface类型的值但是其底层真实类型是`*net.TCPConn`代表一个TCP链接。一个TCP链接有读和写两个部分可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码只关闭网络接中写的部分这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。要在reverb2服务器也完成同样的功能是比较困难的参考**练习 8.4**。)
**练习 8.3** 在netcat3例子中conn虽然是一个interface类型的值但是其底层真实类型是`*net.TCPConn`代表一个TCP连接。一个TCP连接有读和写两个部分可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码只关闭网络接中写的部分这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。要在reverb2服务器也完成同样的功能是比较困难的参考**练习 8.4**。)

View File

@ -1,6 +1,6 @@
### 8.4.2. 串联的ChannelsPipeline
Channels也可以用于将多个goroutine链接在一起一个Channels的输出作为下一个Channels的输入。这种串联的Channels就是所谓的管道pipeline。下面的程序用两个channels将三个goroutine串联起来如图8.1所示。
Channels也可以用于将多个goroutine连接在一起一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道pipeline。下面的程序用两个channels将三个goroutine串联起来如图8.1所示。
![](../images/ch8-01.png)
@ -60,7 +60,7 @@ go func() {
}()
```
因为上面的语法是笨拙的,而且这种处理模式很场景因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法它依次从channel接收数据当channel被关闭并且没有值可接收时跳出循环。
因为上面的语法是笨拙的,而且这种处理模式很常见因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法它依次从channel接收数据当channel被关闭并且没有值可接收时跳出循环。
在下面的改进中我们的计数器goroutine只生成100个含数字的序列然后关闭naturals对应的channel这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。在一个更复杂的程序中可以通过defer语句关闭对应的channel。最后主goroutine也可以正常终止循环并退出程序。

View File

@ -1,6 +1,6 @@
### 8.4.3. 单方向的Channel
随着程序的增长人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine然后用两个channels连接它们它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法
随着程序的增长人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine然后用两个channels连接它们它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法
```Go
func counter(out chan int)
@ -8,7 +8,7 @@ func squarer(out, in chan int)
func printer(in chan int)
```
其中squarer计算平方的函数在两个串联Channels的中间因此拥有两个channels类型的参数,一个用于输入一个用于输出。每个channels都用有相同的类型但是它们的使用方式相反一个只用于接收另一个只用于发送。参数的名字in和out已经明确表示了这个意图但是并无法保证squarer函数向一个in参数对应的channels发送数据或者从一个out参数对应的channels接收数据。
其中计算平方的squarer函数在两个串联Channels的中间因此拥有两个channel类型的参数一个用于输入一个用于输出。两个channel都拥有相同的类型但是它们的使用方式相反一个只用于接收另一个只用于发送。参数的名字in和out已经明确表示了这个意图但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。
这种场景是典型的。当一个channel作为一个函数参数时它一般总是被专门用于只发送或者只接收。
@ -49,6 +49,6 @@ func main() {
}
```
调用counter(naturals)将导致将`chan int`类型的naturals隐式地转换为`chan<- int`channelprinter(squares)`<-chan int`channelchannelchannel`chan<- int`channel`chan int`channel
调用counter(naturals)将导致将`chan int`类型的naturals隐式地转换为`chan<- int`channelprinter(squares)`<-chan int`channelchannelchannel`chan<- int`channel`chan int`channel

View File

@ -28,7 +28,7 @@ ch <- "C"
fmt.Println(<-ch) // "A"
```
那么channel的缓存队列将不是满的也不是空的图8.4因此对该channel执行的发送或接收操作都不会发阻塞。通过这种方式channel的缓存队列解耦了接收和发送的goroutine。
那么channel的缓存队列将不是满的也不是空的图8.4因此对该channel执行的发送或接收操作都不会发阻塞。通过这种方式channel的缓存队列解耦了接收和发送的goroutine。
![](../images/ch8-04.png)
@ -69,7 +69,7 @@ func request(hostname string) (response string) { /* ... */ }
如果我们使用了无缓存的channel那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况称为goroutines泄漏这将是一个BUG。和垃圾变量不同泄漏的goroutines并不会被自动回收因此确保每个不再需要的goroutine能正常退出是重要的。
关于无缓存或带缓存channels之间的选择或者是带缓存channels的容量大小的选择都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作但是对于带缓存channel这些操作是解耦的。同样即使我们知道将要发送到一个channel的信息的数量上限创建一个对应容量大小带缓存channel也是不现实的因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。
关于无缓存或带缓存channels之间的选择或者是带缓存channels的容量大小的选择都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作但是对于带缓存channel这些操作是解耦的。同样即使我们知道将要发送到一个channel的信息的数量上限创建一个对应容量大小带缓存channel也是不现实的因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师一个烘焙一个上糖衣还有一个将每个蛋糕传递到它下一个厨师在生产线。在狭小的厨房空间环境每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它这类似于在一个无缓存的channel上进行沟通。
@ -77,7 +77,7 @@ Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
我们没有太多的空间展示全部细节但是gopl.io/ch8/cake包模拟了这个蛋糕店可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试§11.4

View File

@ -1,6 +1,6 @@
## 8.4. Channels
如果说goroutine是Go语言程序的并发体的话那么channels则是它们之间的通信机制。一个channels是一个通信机制它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
如果说goroutine是Go语言程序的并发体的话那么channels则是它们之间的通信机制。一个channel是一个通信机制它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
使用内置的make函数我们可以创建一个channel
@ -8,11 +8,11 @@
ch := make(chan int) // ch has type 'chan int'
```
和map类似channel也一个对应make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时我们只是拷贝了一个channel引用因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样channel的零值也是nil。
和map类似channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时我们只是拷贝了一个channel引用因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样channel的零值也是nil。
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象那么比较的结果为真。一个channel也可以和nil进行比较。
一个channel有发送和接受两个主要操作都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都用`<-``<-`channel`<-`channel使
一个channel有发送和接受两个主要操作都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用`<-``<-`channel`<-`channel使
```Go
ch <- x // a send statement
@ -20,7 +20,7 @@ x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded
```
Channel还支持close操作用于关闭channel随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据如果channel中已经没有数据的话产生一个零值的数据。
Channel还支持close操作用于关闭channel随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据如果channel中已经没有数据的话产生一个零值的数据。
使用内置的close函数就可以关闭一个channel
@ -28,7 +28,7 @@ Channel还支持close操作用于关闭channel随后对基于该channel的
close(ch)
```
以最简单方式调用make函数创建的时一个无缓存的channel但是我们也可以指定第二个整形参数对应channel的容量。如果channel的容量大于零那么该channel就是带缓存的channel。
以最简单方式调用make函数创建的是一个无缓存的channel但是我们也可以指定第二个整型参数对应channel的容量。如果channel的容量大于零那么该channel就是带缓存的channel。
```Go
ch = make(chan int) // unbuffered channel

View File

@ -26,7 +26,7 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。
@ -39,7 +39,7 @@ func makeThumbnails2(filenames []string) {
}
```
这个版本运行的实在有点太快实际上由于它比最早的版本使用的时间要短得多即使当文件名的slice中只包含有一个元素。这就有点奇怪了如果程序没有并发执行的话那为什么一个并发的版本还是要快呢答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine一个文件名对应一个,但没有等待它们一直到执行完毕。
这个版本运行的实在有点太快实际上由于它比最早的版本使用的时间要短得多即使当文件名的slice中只包含有一个元素。这就有点奇怪了如果程序没有并发执行的话那为什么一个并发的版本还是要快呢答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine一个文件名对应一个,但没有等待它们一直到执行完毕。
没有什么直接的办法能够等待goroutine完成但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓使用的方式是向一个共享的channel中发送事件。因为我们已经知道内部的goroutine只有len(filenames)所以外部的goroutine只需要在返回之前对这些事件计数。
@ -137,7 +137,7 @@ func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里而是通过一个string的channel传过来所以我们无法对循环的次数进行预测。
为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始)我们需要一个递增的计数器在每一个goroutine启动时加一在goroutine退出时减一。这需要一种特殊的计数器这个计数器需要在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup下面的代码就用到了这种方法
为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始)我们需要一个递增的计数器在每一个goroutine启动时加一在goroutine退出时减一。这需要一种特殊的计数器这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup下面的代码就用到了这种方法
```go
// makeThumbnails6 makes thumbnails for each file received from the channel.
@ -176,7 +176,7 @@ func makeThumbnails6(filenames <-chan string) int64 {
注意Add和Done方法的不对称。Add是为计数器加一必须在worker goroutine开始之前调用而不是在goroutine中否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前被调用。并且Add还有一个参数但Done却没有任何参数其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环但又不知道迭代次数时很通常而且很地道的写法。
sizes channel携带了每一个文件的大小到main goroutine在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine并让其等待worker们在关闭掉sizes channel之前退出的。两步操作wait和close必须是基于sizes的循环的并发。考虑一下另一种方案如果等待操作被放在了main goroutine中在循环之前这样的话就永远都不会结束了如果在循环之后那么又变成了不可达的部分因为没有任何东西去关闭这个channel这个循环就永远都不会终止。
sizes channel携带了每一个文件的大小到main goroutine在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作wait和close必须是基于sizes的循环的并发。考虑一下另一种方案如果等待操作被放在了main goroutine中在循环之前这样的话就永远都不会结束了如果在循环之后那么又变成了不可达的部分因为没有任何东西去关闭这个channel这个循环就永远都不会终止。
图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环等待worker发送值或者closer来关闭channel的。