修正半角标点符号

This commit is contained in:
kimw
2018-06-09 16:27:25 +00:00
parent a81bfabcdc
commit 7ebd75aeae
60 changed files with 132 additions and 132 deletions

View File

@@ -49,9 +49,9 @@ 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命令),这个工具可以用来执行网络连接操作。
为了连接例子里的服务器我们需要一个客户端程序比如netcat这个工具nc命令,这个工具可以用来执行网络连接操作。
```
$ go build gopl.io/ch8/clock1

View File

@@ -46,7 +46,7 @@ func main() {
}
```
当main goroutine从标准输入流中读取内容并将其发送给服务器时另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时例如用户在终端中按了Control-D(^D)在windows上是Control-Z这时程序就会被终止尽管其它goroutine中还有进行中的任务。(在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束)
当main goroutine从标准输入流中读取内容并将其发送给服务器时另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时例如用户在终端中按了Control-D(^D)在windows上是Control-Z这时程序就会被终止尽管其它goroutine中还有进行中的任务。在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。
下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。
客户端会向服务器“喊三次话”:

View File

@@ -26,7 +26,7 @@ func makeThumbnails(filenames []string) {
}
```
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题(译注embarrassingly parallel直译的话更像是尴尬并行)。易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。
显然我们处理文件的顺序无关紧要,因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题译注embarrassingly parallel直译的话更像是尴尬并行。易并行问题是最容易被实现成并行的一类问题废话,并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。
下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。
@@ -71,7 +71,7 @@ for _, f := range filenames {
}
```
回忆一下之前在5.6.1节中匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循环所以当这些goroutine开始读取f的值时它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数我们能够确保使用的f是当go语句执行时的“当前”那个f。
回忆一下之前在5.6.1节中匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时for循环可能已经更新了f并且开始了另一轮的迭代或者更有可能的已经结束了整个循环所以当这些goroutine开始读取f的值时它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数我们能够确保使用的f是当go语句执行时的“当前”那个f。
如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢当我们调用thumbnail.ImageFile创建文件失败的时候它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误
@@ -98,9 +98,9 @@ func makeThumbnails4(filenames []string) error {
}
```
这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时都会永远地阻塞下去并且永远都不会退出。这种情况叫做goroutine泄露(§8.4.4)可能会导致整个程序卡住或者跑出out of memory的错误。
这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时都会永远地阻塞下去并且永远都不会退出。这种情况叫做goroutine泄露§8.4.4可能会导致整个程序卡住或者跑出out of memory的错误。
最简单的解决办法就是用一个具有合适大小的buffered channel这样这些worker goroutine向channel中发送错误时就不会被阻塞。(一个可选的解决办法是创建一个另外的goroutine当main goroutine返回第一个错误的同时去排空channel)
最简单的解决办法就是用一个具有合适大小的buffered channel这样这些worker goroutine向channel中发送错误时就不会被阻塞。一个可选的解决办法是创建一个另外的goroutine当main goroutine返回第一个错误的同时去排空channel。)
下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字附带生成时的错误。
@@ -135,9 +135,9 @@ func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
}
```
我们最后一个版本的makeThumbnails返回了新文件们的大小总计数(bytes)。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里而是通过一个string的channel传过来所以我们无法对循环的次数进行预测。
我们最后一个版本的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.

View File

@@ -38,7 +38,7 @@ func main() {
}
```
注意这里的crawl所在的goroutine会将link作为一个显式的参数传入来避免“循环变量快照”的问题(在5.6.1中有讲解)。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容却没有一端接收内容时发生死锁。当然这里我们也可以用buffered channel来解决问题这里不再赘述。
注意这里的crawl所在的goroutine会将link作为一个显式的参数传入来避免“循环变量快照”的问题在5.6.1中有讲解。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容却没有一端接收内容时发生死锁。当然这里我们也可以用buffered channel来解决问题这里不再赘述。
现在爬虫可以高并发地运行起来并且可以产生一大坨的URL了不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的
@@ -58,9 +58,9 @@ 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{}来作为其元素。
我们可以用一个有容量限制的buffered channel来控制并发这类似于操作系统里的计数信号量概念。从概念上讲channel里的n个空槽代表n个可以处理内容的token通行证从channel里接收一个值会释放其中的一个token并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。这里可能我们拿channel里填充的槽来做token更直观一些不过还是这样吧。由于channel里的元素类型并不重要我们用一个零值的struct{}来作为其元素。
让我们重写crawl函数将对links.Extract的调用操作用获取、释放token的操作包裹起来来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。
@@ -82,7 +82,7 @@ func crawl(url string) []string {
}
```
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。(当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题)。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。
第二个问题是这个程序永远都不会终止,即使它已经爬到了所有初始链接衍生出的链接。当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制,你应该还没有意识到这个问题。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。
```go
func main() {
@@ -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

@@ -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 {
@@ -102,7 +102,7 @@ func main() {
}
```
time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine每次被唤醒时发送一个事件。当countdown函数返回时它会停止从tick中接收事件但是ticker这个goroutine还依然存活继续徒劳地尝试向channel中发送值然而这时候已经没有其它的goroutine会从该channel中接收值了--这被称为goroutine泄露(§8.4.4)
time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine每次被唤醒时发送一个事件。当countdown函数返回时它会停止从tick中接收事件但是ticker这个goroutine还依然存活继续徒劳地尝试向channel中发送值然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露§8.4.4
Tick函数挺方便但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话我们应该使用下面的这种模式

View File

@@ -131,7 +131,7 @@ $ ./du2 -v $HOME /usr /bin /etc
213201 files 62.7 GB
```
然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir从而发挥磁盘系统的并行性能。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup (§8.5)来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。
然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir从而发挥磁盘系统的并行性能。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup§8.5来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。
<u><i>gopl.io/ch8/du3</i></u>
```go

View File

@@ -24,7 +24,7 @@ func cancelled() bool {
}
```
下面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键)这个goroutine就会把取消消息通过关闭done的channel广播出去。
下面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到比如用户按了回车键这个goroutine就会把取消消息通过关闭done的channel广播出去。
```go
// Cancel traversal when input is detected.
@@ -81,7 +81,7 @@ func dirents(dir string) []os.FileInfo {
}
```
现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况。
现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源译注:因为程序都退出了,你的代码都没法执行了。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况。
**练习 8.10** HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。提示http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之设置它的Cancel字段然后用http.DefaultClient.Do(req)来进行这个http请求。

View File

@@ -23,7 +23,7 @@ func main() {
}
```
然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的"资格"信息。
然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的资格信息。
```go
type client chan<- string // an outgoing message channel
@@ -112,7 +112,7 @@ 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事件通知当前所有的客户端。这需要你在clients集合中以及entering和leaving的channel中记录客户端的名字。

View File

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