第8章,部分字词修订,语序调整。

This commit is contained in:
zhliner 2017-08-24 22:29:40 +08:00
parent 17919273e1
commit 69606d498b
11 changed files with 29 additions and 30 deletions

View File

@ -49,7 +49,7 @@ Listen函数创建了一个net.Listener的对象这个对象会监听一个
handleConn函数会处理一个完整的客户端连接。在一个for死循环中用time.Now()获取当前时刻然后写到客户端。由于net.Conn实现了io.Writer接口我们可以直接向其写入内容。这个死循环会一直执行直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接然后返回到主函数继续等待下一个连接请求。
time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板标识如何来格式化时间而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几,月份,一个月的第几天,等等)。可以以任意的形式来组合前面这个模板出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式比如time.RFC1123。在进行格式化的逆向操作time.Parse时也会用到同样的策略。(译注这是go语言和其它语言相比比较奇葩的一个地方。。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700而不像其它语言那样Y-m-d H:i:s一样当然了这里可以用1234567的方式来记忆倒是也不麻烦)
time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板标识如何来格式化时间而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几,月份,一个月的第几天,等等)。可以以任意的形式来组合前面这个模板出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式比如time.RFC1123。在进行格式化的逆向操作time.Parse时也会用到同样的策略。(译注这是go语言和其它语言相比比较奇葩的一个地方。。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700而不像其它语言那样Y-m-d H:i:s一样当然了这里可以用1234567的方式来记忆倒是也不麻烦)
为了连接例子里的服务器我们需要一个客户端程序比如netcat这个工具(nc命令),这个工具可以用来执行网络连接操作。
@ -147,7 +147,7 @@ $ ./netcat1
$ killall clock2
```
**练习 8.1** 修改clock2来支持传入参数作为端口号然后写一个clockwall的程序这个程序可以同时与多个clock服务器通信从多服务器中读取时间并且在一个表格中一次显示所有服务传回的结果类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话让这些服务器跑在不同的机器上面或者在同一台机器上跑多个不同的实例这些实例监听不同的端口假装自己在不同的时区。像下面这样
**练习 8.1** 修改clock2来支持传入参数作为端口号然后写一个clockwall的程序这个程序可以同时与多个clock服务器通信从多服务器中读取时间,并且在一个表格中一次显示所有服务传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:
```
$ TZ=US/Eastern ./clock2 -port 8010 &
@ -156,4 +156,4 @@ $ 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.2** 实现一个并发FTP服务器。服务器应该解析客户端来的一些命令比如cd命令来切换目录ls来列出目录内文件get和send来传输文件close来关闭连接。你可以用标准的ftp命令来作为客户端或者也可以自己实现一个。

View File

@ -29,7 +29,7 @@ func main() {
}
```
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-le结束的信号。关闭网络连接中读方向的连接将导致后台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”消息。

View File

@ -93,7 +93,7 @@ func main() {
}
```
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭当它没有被引用时将会被Go语言的垃圾自动回收器回收。不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件都需要在不使用的使用调用对应的Close方法来关闭文件。
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭当它没有被引用时将会被Go语言的垃圾自动回收器回收。不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件都需要在不使用的时候调用对应的Close方法来关闭文件。
试图重复关闭一个channel将导致panic异常试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制我们将在8.9节讨论。

View File

@ -69,15 +69,15 @@ 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上进行沟通。
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师一个烘焙一个上糖衣还有一个将每个蛋糕传递到它下一个厨师生产线。在狭小的厨房空间环境每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它这类似于在一个无缓存的channel上进行沟通。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕制作中这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近那么其中大部分的传输工作将是迅速的个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动例如一个厨师可以短暂地休息然后再加快赶上进度而不影响其他人。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕制作中这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近那么其中大部分的传输工作将是迅速的个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动例如一个厨师可以短暂地休息然后再加快赶上进度而不影响其他人。
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第三阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第三阶段厨师的需求。要解决这个问题我们可以雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。
我们没有太多的空间展示全部细节但是gopl.io/ch8/cake包模拟了这个蛋糕店可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试§11.4

View File

@ -26,7 +26,7 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受并发带来的好处,能够随着并行的规模线性地扩展。
下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。

View File

@ -1,6 +1,6 @@
## 8.6. 示例: 并发的Web爬虫
在5.6节中我们做了一个简单的web爬虫用bfs(广度优先)算法来抓取整个网站。在本节中,我们会让这个这个爬虫并行化这样每一个彼此独立的抓取命令可以并行进行IO最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。
在5.6节中我们做了一个简单的web爬虫用bfs(广度优先)算法来抓取整个网站。在本节中我们会让这个爬虫并行化这样每一个彼此独立的抓取命令可以并行进行IO最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。
<u><i>gopl.io/ch8/crawl1</i></u>
```go
@ -58,7 +58,7 @@ https://golang.org/blog/
最初的错误信息是一个让人莫名的DNS查找失败即使这个域名是完全可靠的。而随后的错误信息揭示了原因这个程序一次性创建了太多网络连接超过了每一个进程的打开文件数限制既而导致了在调用net.Dial像DNS查找失败这样的问题。
这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些限制因素比如CPU核心数会限制你的计算负载比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率比如你的网络带宽限制了你的下载速度上限或者是你的一个web服务的服务容量上限等等。为了解决这个问题我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用这里的n一般小于文件描述符的上限值比如20。这和一个夜店里限制客人数目是一个道理只有当有客人离开时才会允许新的客人进入店内(译注:作者你个老流氓)。
这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情,因为不管怎么说,你的系统总是会有一些限制因素比如CPU核心数会限制你的计算负载比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率比如你的网络带宽限制了你的下载速度上限或者是你的一个web服务的服务容量上限等等。为了解决这个问题我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用这里的n一般小于文件描述符的上限值比如20。这和一个夜店里限制客人数目是一个道理只有当有客人离开时才会允许新的客人进入店内(译注:……)。
我们可以用一个有容量限制的buffered channel来控制并发这类似于操作系统里的计数信号量概念。从概念上讲channel里的n个空槽代表n个可以处理内容的token(通行证)从channel里接收一个值会释放其中的一个token并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。(这里可能我们拿channel里填充的槽来做token更直观一些不过还是这样吧~)。由于channel里的元素类型并不重要我们用一个零值的struct{}来作为其元素。
@ -111,7 +111,7 @@ func main() {
}
```
这个版本中,计器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时我们都会对n进行++操作在向worklist中发送初始的命令行参数之前我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止这时候说明没活可干了。
这个版本中,计器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时我们都会对n进行++操作在向worklist中发送初始的命令行参数之前我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止这时候说明没活可干了。
现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍而且不会出什么错并且在其完成任务时也会正确地终止。
@ -151,13 +151,13 @@ func main() {
所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。
seen这个map被限定在main goroutine中也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式这样的约束可以让我们从一定程度上保证程序的正确性。例如内部变量不能够在函数外部被访问到变量(§2.3.4)在没有被转的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。
seen这个map被限定在main goroutine中也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式这样的约束可以让我们从一定程度上保证程序的正确性。例如内部变量不能够在函数外部被访问到变量(§2.3.4)在没有被转的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。
crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅这个例子的终止问题我们先不进行详细阐述了。
**练习 8.6** 为并发爬虫增加深度限制。也就是说如果用户设置了depth=3那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。
**练习 8.7** 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org结尾,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。
**练习 8.7** 完成一个并发程序来创建一个线上网站的本地镜像,把该站点的所有可达的页面都抓取到本地硬盘。为了省事,我们这里可以只取出现在该域下的所有页面(比如golang.org开头,译注:外链的应该就不算了。)当然了,出现在页面里的链接你也需要进行一些处理,使其能够在你的镜像站点上进行跳转,而不是指向原始的链接。
**译注:**

View File

@ -15,7 +15,7 @@ func main() {
}
```
现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。
现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。
<u><i>gopl.io/ch8/countdown2</i></u>
```go
@ -26,7 +26,7 @@ go func() {
}()
```
现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了ticker channel当一切正常时(就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了为了能够多路复用我们使用了select语句。
现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了当一切正常时的ticker channel就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了为了能够多路复用我们使用了select语句。
```go
select {
@ -41,11 +41,11 @@ default:
}
```
上面是select语句的一般形式。和switch语句稍微有点相似也会有几个case和最后的default选择支。每一个case代表一个通信操作(在某个channel上进行发送或者接收)并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的)就像上面的第一个case或者包含在一个简短的变量声明中像第二个case里一样第二种形式让你能够引用接收到的值。
上面是select语句的一般形式。和switch语句稍微有点相似也会有几个case和最后的default选择分支。每一个case代表一个通信操作在某个channel上进行发送或者接收并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(译注:不把接收到的值赋值给变量什么的)就像上面的第一个case或者包含在一个简短的变量声明中像第二个case里一样第二种形式让你能够引用接收到的值。
select会等待case中有能够执行的case时去执行。当条件满足时select才会去通信并执行case之后的语句这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待到两个事件中的一个到达无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入那么火箭就会发射。
让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待到两个事件中的一个到达无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入那么火箭就会发射。
```go
func main() {

View File

@ -28,7 +28,7 @@ func dirents(dir string) []os.FileInfo {
}
```
ioutil.ReadDir函数会返回一个os.FileInfo类型的sliceos.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言walkDir会递归地调用其自身并且会对每一个文件也递归调用。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。
ioutil.ReadDir函数会返回一个os.FileInfo类型的sliceos.FileInfo类型也是os.Stat这个函数的返回值。对每一个子目录而言walkDir会递归地调用其自身同时也在递归里获取每一个文件的信息。walkDir函数会向fileSizes这个channel发送一条消息。这条消息包含了文件的字节大小。
下面的主函数用了两个goroutine。后台的goroutine调用walkDir来遍历命令行给出的每一个路径并最终关闭fileSizes这个channel。主goroutine会对其从channel中接收到的文件大小进行累加并输出其和。

View File

@ -8,7 +8,7 @@ Go语言并没有提供在一个goroutine中终止另一个goroutine的方法
回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值操作channel之后的代码可以立即被执行并且会产生零值。我们可以将这个机制扩展一下来作为我们的广播机制不要向channel发送值而是用关闭一个channel来进行广播。
只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel这个channel不会向其中发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态。
只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel不需要向这个channel发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态。
<u><i>gopl.io/ch8/du4</i></u>
```go

View File

@ -55,9 +55,9 @@ func broadcaster() {
}
```
broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时会更新clients集合当该事件是离开行为时它会关闭客户端的消息发channel。broadcaster也会监听全局的消息channel所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时就会将其广播至所有连接到服务端的客户端。
broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时会更新clients集合当该事件是离开行为时它会关闭客户端的消息发channel。broadcaster也会监听全局的消息channel所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时就会将其广播至所有连接到服务端的客户端。
现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本并通过全局的消息channel来将这些文本发送出去并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。
现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本并通过全局的消息channel来将这些文本发送出去并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。
```go
func handleConn(conn net.Conn) {
@ -87,7 +87,7 @@ func clientWriter(conn net.Conn, ch <-chan string) {
}
```
另外handleConn为每一个客户端创建了一个clientWriter的goroutine来接收向客户端发出消息channel中发送的广播消息,并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。
另外handleConn为每一个客户端创建了一个clientWriter的goroutine用来接收向客户端发送消息的channel中的广播消息并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。
下面演示的是当服务器有两个活动的客户端连接并且在两个窗口中运行的情况使用netcat来聊天
@ -99,8 +99,7 @@ $ ./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!
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
@ -113,12 +112,12 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
127.0.0.1:64211 has left”
```
当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地解约束并发安全以及goroutine中共享变量的含义。
当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地解约束并发安全以及goroutine中共享变量的含义。
**练习 8.12** 使broadcaster能够将arrival事件通知当前所有的客户端。为了达成这个目的你需要有一个客户端的集合并且在entering和leaving的channel中记录客户端的名字。
**练习 8.13** 使聊天服务器能够断开空闲的客户端连接比如最近五分钟之后没有发送任何消息的那些客户端。提示可以在其它goroutine中调用conn.Close()来解除Read调用就像input.Scanner()所做的那样。
**练习 8.14** 修改聊天服务器的网络协议这样每一个客户端就可以在entering时可以提供它们的名字。将消息前缀由之前的网络地址改为这个名字。
**练习 8.14** 修改聊天服务器的网络协议这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。
**练习 8.15** 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息而不是等待这个客户端一直到其准备好写。或者为每一个客户端的消息发出channel建立缓冲区这样大部分的消息便不会被丢掉broadcaster应该用一个非阻塞的send向这个channel中发消息。
**练习 8.15** 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区这样大部分的消息便不会被丢掉broadcaster应该用一个非阻塞的send向这个channel中发消息。

View File

@ -1,6 +1,6 @@
# 第八章 Goroutines和Channels
并发程序指同时进行多个任务的程序随着硬件的发展并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题--读取数据,计算,写输出--现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。
并发程序指同时进行多个任务的程序随着硬件的发展并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出,现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。
Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型在这种编程模型中值会在不同的运行实例(goroutine)中传递尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型多线程共享内存如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。