diff --git a/eBook/14.0.md b/eBook/14.0.md index f54400b..a4c221a 100644 --- a/eBook/14.0.md +++ b/eBook/14.0.md @@ -1,6 +1,6 @@ -# 14.0 协程(goroutine)与通道(channel) +# 14.0 协程 (goroutine) 与通道 (channel) -作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参见第 15 章)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是协程 (goroutine) 与通道 (channel) 。他们需要语言,编译器,和 runtime 的支持。Go 语言提供的垃圾回收器对并发编程至关重要。 +作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参见[第 15 章](15.0.md)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和 runtime 的支持。Go 语言提供的垃圾回收器对并发编程至关重要。 **不要通过共享内存来通信,而通过通信来共享内存。** diff --git a/eBook/14.1.md b/eBook/14.1.md index 4d835b9..1fdfff7 100644 --- a/eBook/14.1.md +++ b/eBook/14.1.md @@ -6,7 +6,7 @@ 并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。 -公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作 `竞态`)。 +公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作*竞态*)。 **不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。** @@ -48,17 +48,17 @@ Go 的并发原语提供了良好的并发设计基础:表达程序结构以 ## 14.1.3 使用 GOMAXPROCS -在 gc 编译器下(6g 或者 8g)你必须设置 GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 `gccgo` 编译器 GOMAXPROCS 有效的与运行中的协程数量相等。假设 n 是机器上处理器或者核心的数量。如果你设置环境变量 GOMAXPROCS>=n,或者执行 `runtime.GOMAXPROCS(n)`,接下来协程会被分割(分散)到 n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。 +在 gc 编译器下(6g 或者 8g)你必须设置 `GOMAXPROCS` 为一个大于默认值 `1` 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 `GOMAXPROCS` 设置为一个大于 1 的数。当 `GOMAXPROCS` 大于 1 时,会有一个线程池管理许多的线程。通过 `gccgo` 编译器 `GOMAXPROCS` 有效的与运行中的协程数量相等。假设 `n` 是机器上处理器或者核心的数量。如果你设置环境变量 `GOMAXPROCS>=n`,或者执行 `runtime.GOMAXPROCS(n)`,接下来协程会被分割(分散)到 `n` 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 `GOMAXPROCS` 为 `n-1` 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > `1 + GOMAXPROCS` > 1。 -所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS! +所以如果在某一时间只有一个协程在执行,不要设置 `GOMAXPROCS`! -还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 GOMAXPROCS 到 9 会带来性能提升。在一台 32 核的机器上,设置 GOMAXPROCS=8 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 GOMAXPROCS 只会带来轻微的性能下降;设置 GOMAXPROCS=100,使用 `top` 命令和 `H` 选项查看到只有 7 个活动的线程。 +还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 `GOMAXPROCS` 到 9 会带来性能提升。在一台 32 核的机器上,设置 `GOMAXPROCS=8` 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 `GOMAXPROCS` 只会带来轻微的性能下降;设置 `GOMAXPROCS=100`,使用 `top` 命令和 `H` 选项查看到只有 7 个活动的线程。 -增加 GOMAXPROCS 的数值对程序进行并发计算是有好处的; +增加 `GOMAXPROCS` 的数值对程序进行并发计算是有好处的; 请看 [goroutine_select2.go](examples/chapter_14/goroutine_select2.go) -总结:GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于1个的机器上,会尽可能有等同于核心数的线程在并行运行。 +总结:`GOMAXPROCS` 等同于(并发的)线程数量,在一台核心数多于 1 个的机器上,会尽可能有等同于核心数的线程在并行运行。 ## 14.1.4 如何用命令行指定使用的核心数量 @@ -73,7 +73,7 @@ flag.Parse() runtime.GOMAXPROCS(*numCores) ``` -协程可以通过调用`runtime.Goexit()`来停止,尽管这样做几乎没有必要。 +协程可以通过调用 `runtime.Goexit()` 来停止,尽管这样做几乎没有必要。 示例 14.1-[goroutine1.go](examples/chapter_14/goroutine1.go) 介绍了概念: @@ -120,7 +120,7 @@ End of longWait() At the end of main() // after 10s ``` -`main()`,`longWait()` 和 `shortWait()` 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了 `time` 包中的 `Sleep` 函数。`Sleep()` 可以按照指定的时间来暂停函数或协程的执行,这里使用了纳秒(ns,符号 1e9 表示 1 乘 10 的 9 次方,e=指数)。 +`main()`,`longWait()` 和 `shortWait()` 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了 `time` 包中的 `Sleep` 函数。`Sleep()` 可以按照指定的时间来暂停函数或协程的执行,这里使用了纳秒(`ns`,符号 `1e9` 表示 1 乘 10 的 9 次方,`e`=指数)。 他们按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,以并行的方式。我们让 `main()` 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让 `main()` 函数停止 4 秒),`main()` 会提前结束,`longWait()` 则无法完成。如果我们不在 `main()` 中等待,协程会随着程序的结束而消亡。 @@ -128,7 +128,7 @@ At the end of main() // after 10s 另外,协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。 -为了对比使用一个线程,连续调用的情况,移除 go 关键字,重新运行程序。 +为了对比使用一个线程,连续调用的情况,移除 `go` 关键字,重新运行程序。 现在输出: @@ -146,11 +146,11 @@ At the end of main() // after 17 s 将数组分割为若干个不重复的切片,然后给每一个切片启动一个协程进行查找计算。这样许多并行的协程可以用来进行查找任务,整体的查找时间会缩短(除以协程的数量)。 -## 14.1.5 Go 协程(goroutines)和协程(coroutines) +## 14.1.5 Go 协程 (goroutines) 和协程 (coroutines) -(译者注:标题中的“Go协程(goroutines)” 即是 14 章讲的协程指的是 Go 语言中的协程。而“协程(coroutines)”指的是其他语言中的协程概念,仅在本节出现。) +(译者注:标题中的“Go协程 (goroutines)”即是 14 章讲的协程,指的是 Go 语言中的协程。而“协程(coroutines)”指的是其他语言中的协程概念,仅在本节出现。) -在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go协程有些相似,不过有两点不同: +在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go 协程有些相似,不过有两点不同: - Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的 - Go 协程通过通道来通信;协程通过让出和恢复操作来通信 @@ -160,5 +160,5 @@ Go 协程比协程更强大,也很容易从协程的逻辑复用到 Go 协程 ## 链接 - [目录](directory.md) -- 上一节:[协程(goroutine)与通道(channel)](14.0.md) +- 上一节:[协程 (goroutine) 与通道 (channel)](14.0.md) - 下一节:[使用通道进行协程间通信](14.2.md) diff --git a/eBook/14.10.md b/eBook/14.10.md index 52074d1..75ec30e 100644 --- a/eBook/14.10.md +++ b/eBook/14.10.md @@ -4,7 +4,7 @@ 客户端-服务器应用正是 goroutines 和 channels 的亮点所在。 -客户端(Client)可以是运行在任意设备上的任意程序,它会按需发送请求(request)至服务器。服务器(Server)接收到这个请求后开始相应的工作,然后再将响应(response)返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而 Web 服务器则会向浏览器响应网页数据。 +客户端 (Client) 可以是运行在任意设备上的任意程序,它会按需发送请求 (request) 至服务器。服务器 (Server) 接收到这个请求后开始相应的工作,然后再将响应 (response) 返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而 Web 服务器则会向浏览器响应网页数据。 使用 Go 的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。 @@ -23,9 +23,9 @@ type Request struct{ replyc chan *Reply } ``` - -接下来先使用简单的形式,服务器会为每一个请求启动一个协程并在其中执行 `run()` 函数,此举会将类型为 `binOp` 的 `op` 操作返回的 int 值发送到 `replyc` 通道。 + +接下来先使用简单的形式,服务器会为每一个请求启动一个协程并在其中执行 `run()` 函数,此举会将类型为 `binOp` 的 `op` 操作返回的 `int` 值发送到 `replyc` 通道。 ```go @@ -35,7 +35,7 @@ func run(op binOp, req *Request) { req.replyc <- op(req.a, req.b) } ``` -`server` 协程会无限循环以从 `chan *Request` 接收请求,并且为了避免被长时间操作所堵塞,它将为每一个请求启动一个协程来做具体的工作: +`server()` 协程会无限循环以从 `chan *Request` 接收请求,并且为了避免被长时间操作所堵塞,它将为每一个请求启动一个协程来做具体的工作: ```go func server(op binOp, service chan *Request) { @@ -46,7 +46,8 @@ func server(op binOp, service chan *Request) { } } ``` -`server` 本身则是以协程的方式在 `startServer` 函数中启动: +`server()` 本身则是以协程的方式在 `startServer()` 函数中启动: + ```go func startServer(op binOp) chan *Request { reqChan := make(chan *Request); @@ -54,7 +55,7 @@ func startServer(op binOp) chan *Request { return reqChan; } ``` -`startServer` 则会在 `main` 协程中被调用。 +`startServer()` 则会在 `main` 协程中被调用。 在以下测试例子中,100 个请求会被发送到服务器,只有它们全部被送达后我们才会按相反的顺序检查响应: ```go @@ -92,7 +93,6 @@ func main() { Request 0 is ok! done - 这个程序仅启动了 100 个协程。然而即使执行 100,000 个协程我们也能在数秒内看到它完成。这说明了 Go 的协程是如何的轻量:如果我们启动相同数量的真实的线程,程序早就崩溃了。 示例: 14.14-[multiplex_server.go](examples/chapter_14/multiplex_server.go) @@ -148,11 +148,10 @@ func main() { } fmt.Println("done") } - ``` -## 14.10.2 卸载(Teardown):通过信号通道关闭服务器 +## 14.10.2 卸载 (Teardown):通过信号通道关闭服务器 -在上一个版本中 `server` 在 `main` 函数返回后并没有完全关闭,而被强制结束了。为了改进这一点,我们可以提供一个退出通道给 `server` : +在上一个版本中 `server()` 在 `main()` 函数返回后并没有完全关闭,而被强制结束了。为了改进这一点,我们可以提供一个退出通道给 `server()` : ```go func startServer(op binOp) (service chan *Request, quit chan bool) { @@ -163,7 +162,7 @@ func startServer(op binOp) (service chan *Request, quit chan bool) { } ``` -`server` 函数现在则使用 `select` 在 `service` 通道和 `quit` 通道之间做出选择: +`server()` 函数现在则使用 `select` 在 `service` 通道和 `quit` 通道之间做出选择: ```go func server(op binOp, service chan *request, quit chan bool) { @@ -179,13 +178,13 @@ func server(op binOp, service chan *request, quit chan bool) { ``` 当 `quit` 通道接收到一个 `true` 值时,`server` 就会返回并结束。 -在 `main` 函数中我们做出如下更改: +在 `main()` 函数中我们做出如下更改: ```go adder, quit := startServer(func(a, b int) int { return a + b }) ``` -在 `main` 函数的结尾处我们放入这一行:`quit <- true` +在 `main()` 函数的结尾处我们放入这一行:`quit <- true` 完整的代码在 [multiplex_server2.go](examples/chapter_14/multiplex_server2.go),输出和上一个版本是一样的。 diff --git a/eBook/14.2.md b/eBook/14.2.md index db3324c..96fcd61 100644 --- a/eBook/14.2.md +++ b/eBook/14.2.md @@ -14,11 +14,11 @@ 通常使用这样的格式来声明通道:`var identifier chan datatype` -未初始化的通道的值是 nil 。 +未初始化的通道的值是 `nil`。 所以通道只能传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以,甚至可以(有时非常有用)创建通道的通道。 -通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(two-way pipe))。通道也是引用类型,所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化): +通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO) 的结构所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道 (two-way pipe) )。通道也是引用类型,所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化): ```go var ch1 chan string @@ -39,11 +39,11 @@ ch1 = make(chan string) 流向通道(发送) -`ch <- int1` 表示:用通道 ch 发送变量 int1(双目运算符,中缀 = 发送) +`ch <- int1` 表示:用通道 `ch` 发送变量 `int1`(双目运算符,中缀 = 发送) 从通道流出(接收),三种方式: -`int2 = <- ch` 表示:变量 int2 从通道 ch(一元运算的前缀操作符,前缀 = 接收)接收数据(获取新值);假设 int2 已经声明过了,如果没有的话可以写成:`int2 := <- ch`。 +`int2 = <- ch` 表示:变量 `int2` 从通道 ch(一元运算的前缀操作符,前缀 = 接收)接收数据(获取新值);假设 `int2` 已经声明过了,如果没有的话可以写成:`int2 := <- ch`。 `<- ch` 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的: @@ -106,15 +106,15 @@ Washington Tripoli London Beijing tokyo 我们发现协程之间的同步非常重要: -- main() 等待了 1 秒让两个协程完成,如果不这样,sendData() 就没有机会输出。 -- getData() 使用了无限循环:它随着 sendData() 的发送完成和 ch 变空也结束了。 +- `main()` 等待了 1 秒让两个协程完成,如果不这样,`sendData()` 就没有机会输出。 +- `getData()` 使用了无限循环:它随着 `sendData()` 的发送完成和 `ch` 变空也结束了。 - 如果我们移除一个或所有 `go` 关键字,程序无法运行,Go 运行时会抛出 panic: ``` ---- Error run E:/Go/Goboek/code examples/chapter 14/goroutine2.exe with code Crashed ---- Program exited with code -2147483645: panic: all goroutines are asleep-deadlock! ``` -为什么会这样?运行时(runtime)会检查所有的协程(像本例中只有一个)是否在等待着什么东西(可从某个通道读取或者写入某个通道),这意味着程序将无法继续执行。这是死锁(deadlock)的一种形式,而运行时(runtime)可以为我们检测到这种情况。 +为什么会这样?运行时 (runtime) 会检查所有的协程(像本例中只有一个)是否在等待着什么东西(可从某个通道读取或者写入某个通道),这意味着程序将无法继续执行。这是死锁 (deadlock) 的一种形式,而运行时 (runtime) 可以为我们检测到这种情况。 注意:不要使用打印状态来表明通道的发送和接收顺序:由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同。 @@ -124,13 +124,13 @@ Washington Tripoli London Beijing tokyo 默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的: -1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 ch 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。 +1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 `ch` 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 `ch` 再次变为可用状态:就是通道值被接收时(可以传入变量)。 2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。 尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。 -程序 [channel_block.go](examples/chapter_14/channel_block.go) 验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字 0。 +程序 [channel_block.go](examples/chapter_14/channel_block.go) 验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字 `0`。 示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go) @@ -160,7 +160,7 @@ func pump(ch chan int) { `pump()` 函数为通道提供数值,也被叫做生产者。 -为通道解除阻塞定义了 `suck` 函数来在无限循环中读取通道,参见示例 14.4-[channel_block2.go](examples/chapter_14/channel_block2.go): +为通道解除阻塞定义了 `suck()` 函数来在无限循环中读取通道,参见示例 14.4-[channel_block2.go](examples/chapter_14/channel_block2.go): ```go func suck(ch chan int) { @@ -183,9 +183,9 @@ time.Sleep(1e9) ## 14.2.4 通过一个(或多个)通道交换数据进行协程同步。 -通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。 +通信是一种同步形式:通过通道,两个协程在通信(协程会合)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。 -甚至可以在通道两端互相阻塞对方,形成了叫做死锁的状态。Go 运行时会检查并 panic,停止程序。死锁几乎完全是由糟糕的设计导致的。 +甚至可以在通道两端互相阻塞对方,形成了叫做**死锁**的状态。Go 运行时会检查并 `panic()`,停止程序。死锁几乎完全是由糟糕的设计导致的。 无缓冲通道会被阻塞。设计无阻塞的程序可以避免这种情况,或者使用带缓冲的通道。 @@ -220,20 +220,20 @@ buf := 100 ch1 := make(chan string, buf) ``` -buf 是通道可以同时容纳的元素(这里是 string)个数 +`buf` 是通道可以同时容纳的元素(这里是 `string`)个数 在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。 -缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 `cap` 函数可以返回缓冲区的容量。 +缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 `cap()` 函数可以返回缓冲区的容量。 如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。 同步:`ch :=make(chan type, value)` -- value == 0 -> synchronous, unbuffered (阻塞) -- value > 0 -> asynchronous, buffered(非阻塞)取决于 value 元素 +- `value == 0 -> synchronous`, unbuffered (阻塞) +- `value > 0 -> asynchronous`, buffered(非阻塞)取决于 `value` 元素 -若使用通道的缓冲,你的程序会在“请求”激增的时候表现更好:更具弹性,专业术语叫:更具有伸缩性(scalable)。在设计算法时首先考虑使用无缓冲通道,只在不确定的情况下使用缓冲。 +若使用通道的缓冲,你的程序会在“请求”激增的时候表现更好:更具弹性,专业术语叫:更具有伸缩性(scalable)。在设计算法时首先考虑使用无缓冲通道,只在不确定的情况下使用缓冲。 练习 14.3:[channel_buffer.go](exercises/chapter_14/channel_buffer.go):给 [channel_block3.go](exercises/chapter_14/channel_block3.go) 的通道增加缓冲并观察输出有何不同。 @@ -248,15 +248,15 @@ go sum(bigArray, ch) // bigArray puts the calculated sum on ch sum := <- ch // wait for, and retrieve the sum ``` -也可以使用通道来达到同步的目的,这个很有效的用法在传统计算机中称为信号量(semaphore)。或者换个方式:通过通道发送信号告知处理已经完成(在协程中)。 +也可以使用通道来达到同步的目的,这个很有效的用法在传统计算机中称为信号量 (semaphore)。或者换个方式:通过通道发送信号告知处理已经完成(在协程中)。 -在其他协程运行时让 main 程序无限阻塞的通常做法是在 `main` 函数的最后放置一个 `select {}`。 +在其他协程运行时让 `main` 程序无限阻塞的通常做法是在 `main()` 函数的最后放置一个 `select {}`。 也可以使用通道让 `main` 程序等待协程完成,就是所谓的信号量模式,我们会在接下来的部分讨论。 ## 14.2.7 信号量模式 -下边的片段阐明:协程通过在通道 `ch` 中放置一个值来处理结束的信号。`main` 协程等待 `<-ch` 直到从中获取到值。 +下边的片段阐明:协程通过在通道 `ch` 中放置一个值来处理结束的信号。`main()` 协程等待 `<-ch` 直到从中获取到值。 我们期望从这个通道中获取返回的结果,像这样: @@ -273,7 +273,7 @@ func main(){ } ``` -这个信号也可以是其他的,不返回结果,比如下面这个协程中的匿名函数(lambda)协程: +这个信号也可以是其他的,不返回结果,比如下面这个协程中的匿名函数 (lambda) 协程: ```go ch := make(chan int) @@ -285,7 +285,7 @@ doSomethingElseForAWhile() <- ch // Wait for goroutine to finish; discard sent value. ``` -或者等待两个协程完成,每一个都会对切片 s 的一部分进行排序,片段如下: +或者等待两个协程完成,每一个都会对切片 `s` 的一部分进行排序,片段如下: ```go done := make(chan bool) @@ -301,7 +301,7 @@ go doSort(s[i:]) <-done ``` -下边的代码,用完整的信号量模式对长度为 N 的 float64 切片进行了 N 个 `doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(且包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。 +下边的代码,用完整的信号量模式对长度为 `N` 的 `float64` 切片进行了 `N` 个 `doSomething()` 计算并同时完成,通道 `sem` 分配了相同的长度(且包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 `sem` 不停的接收数据来等待所有的协程完成。 ```go type Empty interface {} @@ -321,12 +321,11 @@ for i, xi := range data { for i := 0; i < N; i++ { <-sem } ``` -注意上述代码中闭合函数的用法:`i`、`xi` 都是作为参数传入闭合函数的,这一做法使得每个协程(译者注:在其启动时)获得一份 `i` 和 `xi` 的单独拷贝,从而向闭合函数内部屏蔽了外层循环中的 `i` 和 `xi` 变量;否则,for 循环的下一次迭代会更新所有协程中 `i` 和 `xi` 的值。另一方面,切片 `res` 没有传入闭合函数,因为协程不需要 `res` 的单独拷贝。切片 `res` 也在闭合函数中但并不是参数。 +注意上述代码中闭合函数的用法:`i`、`xi` 都是作为参数传入闭合函数的,这一做法使得每个协程(译者注:在其启动时)获得一份 `i` 和 `xi` 的单独拷贝,从而向闭合函数内部屏蔽了外层循环中的 `i` 和 `xi` 变量;否则,`for` 循环的下一次迭代会更新所有协程中 `i` 和 `xi` 的值。另一方面,切片 `res` 没有传入闭合函数,因为协程不需要 `res` 的单独拷贝。切片 `res` 也在闭合函数中但并不是参数。 +## 14.2.8 实现并行的 `for` 循环 -## 14.2.8 实现并行的 for 循环 - -在上一部分章节 [14.2.7](14.2.md#1427-信号量模式) 的代码片段中:for 循环的每一个迭代是并行完成的: +在上一部分章节 [14.2.7](14.2.md#1427-信号量模式) 的代码片段中:`for` 循环的每一个迭代是并行完成的: ```go for i, v := range data { @@ -337,7 +336,7 @@ for i, v := range data { } ``` -在 for 循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如 Fortress 或者其他并行框架以不同的结构实现了这种方式,在 Go 中用协程实现起来非常容易: +在 `for` 循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如 Fortress 或者其他并行框架以不同的结构实现了这种方式,在 Go 中用协程实现起来非常容易: ## 14.2.9 用带缓冲通道实现一个信号量 @@ -354,7 +353,7 @@ type Empty interface {} type semaphore chan Empty ``` -将可用资源的数量N来初始化信号量 `semaphore`:`sem = make(semaphore, N)` +将可用资源的数量 `N` 来初始化信号量 `semaphore`:`sem = make(semaphore, N)` 然后直接对信号量进行操作: @@ -399,9 +398,9 @@ func (s semaphore) Signal() { 练习 14.5:[gosum.go](exercises/chapter_14/gosum.go):用这种习惯用法写一个程序,开启一个协程来计算 2 个整数的和并等待计算结果并打印出来。 -练习 14.6:[producer_consumer.go](exercises/chapter_14/producer_consumer.go):用这种习惯用法写一个程序,有两个协程,第一个提供数字 0,10,20,...90 并将他们放入通道,第二个协程从通道中读取并打印。`main()` 等待两个协程完成后再结束。 +练习 14.6:[producer_consumer.go](exercises/chapter_14/producer_consumer.go):用这种习惯用法写一个程序,有两个协程,第一个提供数字 0,10,20,...,90 并将他们放入通道,第二个协程从通道中读取并打印。`main()` 等待两个协程完成后再结束。 -习惯用法:通道工厂模式 +**习惯用法:通道工厂模式** 编程中常见的另外一种模式如下:不将通道作为参数传递给协程,而用函数来生成一个通道并返回(工厂角色);函数内有个匿名函数被协程调用。 @@ -448,7 +447,7 @@ for v := range ch { } ``` -它从指定通道中读取数据直到通道关闭,才继续执行下边的代码。很明显,另外一个协程必须写入 `ch`(不然代码就阻塞在 for 循环了),而且必须在写入完成后才关闭。`suck` 函数可以这样写,且在协程中调用这个动作,程序变成了这样: +它从指定通道中读取数据直到通道关闭,才继续执行下边的代码。很明显,另外一个协程必须写入 `ch`(不然代码就阻塞在 `for` 循环了),而且必须在写入完成后才关闭。`suck()` 函数可以这样写,且在协程中调用这个动作,程序变成了这样: 示例 14.6-[channel_idiom2.go](examples/chapter_14/channel_idiom2.go): @@ -484,9 +483,9 @@ func suck(ch chan int) { } ``` -习惯用法:通道迭代模式 +**习惯用法:通道迭代器模式** -这个模式用到了后边 14.6 章示例 [producer_consumer.go](exercises/chapter_14/producer_consumer.go) 的生产者-消费者模式,通常,需要从包含了地址索引字段 items 的容器给通道填入元素。为容器的类型定义一个方法 `Iter()`,返回一个只读的通道(参见第 [14.2.11](14.2.md#14211-通道的方向) 节)items,如下: +这个模式用到了后边 [14.6 章](14.6.md)示例 [producer_consumer.go](exercises/chapter_14/producer_consumer.go) 的生产者-消费者模式。通常,需要从包含了地址索引字段 `items` 的容器给通道填入元素。为容器的类型定义一个方法 `Iter()`,返回一个只读的通道(参见第 [14.2.11](14.2.md#14211-通道的方向) 节)`items`,如下: ```go func (c *container) Iter () <- chan item { @@ -500,7 +499,7 @@ func (c *container) Iter () <- chan item { } ``` -在协程里,一个 for 循环迭代容器 c 中的元素(对于树或图的算法,这种简单的 for 循环可以替换为深度优先搜索)。 +在协程里,一个 `for` 循环迭代容器 `c` 中的元素(对于树或图的算法,这种简单的 `for` 循环可以替换为深度优先搜索)。 调用这个方法的代码可以这样迭代容器: @@ -508,11 +507,11 @@ func (c *container) Iter () <- chan item { for x := range container.Iter() { ... } ``` -其运行在自己启动的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在不同的线程上)。 这样我们就有了一个典型的生产者-消费者模式。如果在程序结束之前,向通道写值的协程未完成工作,则这个协程不会被垃圾回收;这是设计使然。这种看起来并不符合预期的行为正是由通道这种线程安全的通信方式所导致的。如此一来,一个协程为了写入一个永远无人读取的通道而被挂起就成了一个 bug ,而并非你预想中的那样被悄悄回收掉(garbage-collected)了。 +其运行在自己启动的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在不同的线程上)。 这样我们就有了一个典型的生产者-消费者模式。如果在程序结束之前,向通道写值的协程未完成工作,则这个协程不会被垃圾回收;这是设计使然。这种看起来并不符合预期的行为正是由通道这种线程安全的通信方式所导致的。如此一来,一个协程为了写入一个永远无人读取的通道而被挂起就成了一个 bug ,而并非你预想中的那样被悄悄回收掉 (garbage-collected) 了。 -习惯用法:生产者消费者模式 +**习惯用法:生产者消费者模式** -假设你有 `Produce()` 函数来产生 `Consume` 函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环: +假设你有 `Produce()` 函数来产生 `Consume()` 函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环: ```go for { @@ -529,7 +528,7 @@ var send_only chan<- int // channel can only receive data var recv_only <-chan int // channel can only send data ``` -只接收的通道(<-chan T)无法关闭,因为关闭通道是发送者用来表示不再给通道发送值了,所以对只接收通道是没有意义的。通道创建的时候都是双向的,但也可以分配有方向的通道变量,就像以下代码: +只接收的通道 (`<-chan T`) 无法关闭,因为关闭通道是发送者用来表示不再给通道发送值了,所以对只接收通道是没有意义的。通道创建的时候都是双向的,但也可以分配给有方向的通道变量,就像以下代码: ```go var c = make(chan int) // bidirectional @@ -545,7 +544,7 @@ func sink(ch <-chan int) { } ``` -习惯用法:管道和选择器模式 +**习惯用法:管道和选择器模式** 更具体的例子还有协程处理它从通道接收的数据并发送给输出通道: @@ -610,7 +609,7 @@ func main() { } ``` -协程 `filter(in, out chan int, prime int)` 拷贝整数到输出通道,丢弃掉可以被 prime 整除的数字。然后每个 prime 又开启了一个新的协程,生成器和选择器并发请求。 +协程 `filter(in, out chan int, prime int)` 拷贝整数到输出通道,丢弃掉可以被 `prime` 整除的数字。然后每个 `prime` 又开启了一个新的协程,生成器和选择器并发请求。 输出: @@ -625,7 +624,7 @@ func main() { 937 941 947 953 967 971 977 983 991 997 1009 1013... ``` -第二个版本引入了上边的习惯用法:函数 `sieve`、`generate` 和 `filter` 都是工厂;它们创建通道并返回,而且使用了协程的 lambda 函数。`main` 函数现在短小清晰:它调用 `sieve()` 返回了包含素数的通道,然后通过 `fmt.Println(<-primes)` 打印出来。 +第二个版本引入了上边的习惯用法:函数 `sieve()`、`generate()` 和 `filter()` 都是工厂;它们创建通道并返回,而且使用了协程的 lambda 函数。`main()` 函数现在短小清晰:它调用 `sieve()` 返回了包含素数的通道,然后通过 `fmt.Println(<-primes)` 打印出来。 版本2:示例 14.8-[sieve2.go](examples/chapter_14/sieve2.go) diff --git a/eBook/14.3.md b/eBook/14.3.md index 9eef118..d6798ae 100644 --- a/eBook/14.3.md +++ b/eBook/14.3.md @@ -4,14 +4,14 @@ 继续看示例 [goroutine2.go](examples/chapter_14/goroutine2.go)(示例 14.2):我们如何在通道的 `sendData()` 完成的时候发送一个信号,`getData()` 又如何检测到通道是否关闭或阻塞? -第一个可以通过函数 `close(ch)` 来完成:这个将通道标记为无法通过发送操作 `<-` 接受更多的值;给已经关闭的通道发送或者再次关闭都会导致运行时的 panic。在创建一个通道后使用 defer 语句是个不错的办法(类似这种情况): +第一个可以通过函数 `close(ch)` 来完成:这个将通道标记为无法通过发送操作 `<-` 接受更多的值;给已经关闭的通道发送或者再次关闭都会导致运行时的 `panic()`。在创建一个通道后使用 `defer` 语句是个不错的办法(类似这种情况): ```go ch := make(chan float64) defer close(ch) ``` -第二个问题可以使用逗号,ok 操作符:用来检测通道是否被关闭。 +第二个问题可以使用逗号 ok 模式用来检测通道是否被关闭。 如何来检测可以收到没有被阻塞(或者通道没有被关闭)? @@ -19,7 +19,7 @@ defer close(ch) v, ok := <-ch // ok is true if v received value ``` -通常和 if 语句一起使用: +通常和 `if` 语句一起使用: ```go if v, ok := <-ch; ok { @@ -27,7 +27,7 @@ if v, ok := <-ch; ok { } ``` -或者在 for 循环中接收的时候,当关闭的时候使用 break: +或者在 `for` 循环中接收的时候,当关闭的时候使用 `break`: ```go v, ok := <-ch @@ -37,7 +37,7 @@ if !ok { process(v) ``` -而检测通道当前是否阻塞,需要使用 select(参见第 [14.4](14.4.md) 节)。 +而检测通道当前是否阻塞,需要使用 `select`(参见第 [14.4](14.4.md) 节)。 ```go select { @@ -54,7 +54,7 @@ default: 在示例程序 14.2 中使用这些可以改进为版本 [goroutine3.go](examples/chapter_14/goroutine3.go),输出相同。 -实现非阻塞通道的读取,需要使用 select(参见第 [14.4](14.4.md) 节)。 +实现非阻塞通道的读取,需要使用 `select`(参见第 [14.4](14.4.md) 节)。 示例 14.9-[goroutine3.go](examples/chapter_14/goroutine3.go): @@ -111,7 +111,7 @@ func sendData(ch chan string) { } ``` -- 在 for 循环的 `getData()` 中,在每次接收通道的数据之前都使用 `if !open` 来检测: +- 在 `for` 循环的 `getData()` 中,在每次接收通道的数据之前都使用 `if !open` 来检测: ```go for { @@ -133,7 +133,7 @@ for input := range ch { 阻塞和生产者-消费者模式: -在第 14.2.10 节的通道迭代器中,两个协程经常是一个阻塞另外一个。如果程序工作在多核心的机器上,大部分时间只用到了一个处理器。可以通过使用带缓冲(缓冲空间大于 0)的通道来改善。比如,缓冲大小为 100,迭代器在阻塞之前,至少可以从容器获得 100 个元素。如果消费者协程在独立的内核运行,就有可能让协程不会出现阻塞。 +在[第 14.2.10 节](14.2.md)的通道迭代器中,两个协程经常是一个阻塞另外一个。如果程序工作在多核心的机器上,大部分时间只用到了一个处理器。可以通过使用带缓冲(缓冲空间大于 0)的通道来改善。比如,缓冲大小为 100,迭代器在阻塞之前,至少可以从容器获得 100 个元素。如果消费者协程在独立的内核运行,就有可能让协程不会出现阻塞。 由于容器中元素的数量通常是已知的,需要让通道有足够的容量放置所有的元素。这样,迭代器就不会阻塞(尽管消费者协程仍然可能阻塞)。然而,这实际上加倍了迭代容器所需要的内存使用量,所以通道的容量需要限制一下最大值。记录运行时间和性能测试可以帮助你找到最小的缓存容量带来最好的性能。 diff --git a/eBook/14.4.md b/eBook/14.4.md index 8cb9f19..8d59815 100644 --- a/eBook/14.4.md +++ b/eBook/14.4.md @@ -1,6 +1,6 @@ # 14.4 使用 select 切换协程 -从不同的并发执行的协程中获取值可以通过关键字 `select` 来完成,它和 `switch` 控制语句非常相似(章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select` 监听进入通道的数据,也可以是用通道发送值的时候。 +从不同的并发执行的协程中获取值可以通过关键字 `select` 来完成,它和 `switch` 控制语句非常相似([章节 5.3](05.3.md))也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select` 监听进入通道的数据,也可以是用通道发送值的时候。 ```go select { @@ -14,7 +14,7 @@ default: // no value ready to be received } ``` -`default` 语句是可选的;fallthrough 行为,和普通的 switch 相似,是不允许的。在任何一个 case 中执行 `break` 或者 `return`,select 就结束了。 +`default` 语句是可选的;`fallthrough` 行为,和普通的 `switch` 相似,是不允许的。在任何一个 `case` 中执行 `break` 或者 `return`,select 就结束了。 `select` 做的就是:选择处理列出的多个通信情况中的一个。 @@ -22,11 +22,11 @@ default: // no value ready to be received - 如果多个可以处理,随机选择一个 - 如果没有通道操作可以处理并且写了 `default` 语句,它就会执行:`default` 永远是可运行的(这就是准备好了,可以执行)。 -在 `select` 中使用发送操作并且有 `default` 可以确保发送不被阻塞!如果没有 `default`,select 就会一直阻塞。 +在 `select` 中使用发送操作并且有 `default` 可以确保发送不被阻塞!如果没有 `default`,`select` 就会一直阻塞。 `select` 语句实现了一种监听模式,通常用在(无限)循环中;在某种情况下,通过 `break` 语句使循环退出。 -在程序 [goroutine_select.go](examples/chapter_14/goroutine_select.go) 中有 2 个通道 `ch1` 和 `ch2`,三个协程 `pump1()`、`pump2()` 和 `suck()`。这是一个典型的生产者消费者模式。在无限循环中,`ch1` 和 `ch2` 通过 `pump1()` 和 `pump2()` 填充整数;`suck()` 也是在无限循环中轮询输入的,通过 `select` 语句获取 `ch1` 和 `ch2` 的整数并输出。选择哪一个 case 取决于哪一个通道收到了信息。程序在 main 执行 1 秒后结束。 +在程序 [goroutine_select.go](examples/chapter_14/goroutine_select.go) 中有 2 个通道 `ch1` 和 `ch2`,三个协程 `pump1()`、`pump2()` 和 `suck()`。这是一个典型的生产者消费者模式。在无限循环中,`ch1` 和 `ch2` 通过 `pump1()` 和 `pump2()` 填充整数;`suck()` 也是在无限循环中轮询输入的,通过 `select` 语句获取 `ch1` 和 `ch2` 的整数并输出。选择哪一个 `case` 取决于哪一个通道收到了信息。程序在 `main` 执行 1 秒后结束。 示例 14.10-[goroutine_select.go](examples/chapter_14/goroutine_select.go): @@ -91,14 +91,14 @@ Received on channel 1: 94346 Received on channel 1: 94348 ``` -一秒内的输出非常惊人,如果我们给它计数([goroutine_select2.go](examples/chapter_14/goroutine_select2.go)),得到了 90000 个左右的数字。 +一秒内的输出非常惊人,如果我们给它计数 ([goroutine_select2.go](examples/chapter_14/goroutine_select2.go)),得到了 90000 个左右的数字。 ## 练习: 练习 14.7: -- a)在练习 5.4 的 [for_loop.go](exercises/chapter_5/for_loop.go) 中,有一个常见的 for 循环打印数字。在函数 `tel` 中实现一个 for 循环,用协程开始这个函数并在其中给通道发送数字。`main()` 线程从通道中获取并打印。不要使用 `time.Sleep()` 来同步:[goroutine_panic.go](exercises/chapter_14/goroutine_panic.go) -- b)也许你的方案有效,可能会引发运行时的 panic:`throw:all goroutines are asleep-deadlock!` 为什么会这样?你如何解决这个问题?[goroutine_close.go](exercises/chapter_14/goroutine_close.go) +- a)在练习 5.4 的 [for_loop.go](exercises/chapter_5/for_loop.go) 中,有一个常见的 `for` 循环打印数字。在函数 `tel()` 中实现一个 `for` 循环,用协程开始这个函数并在其中给通道发送数字。`main()` 线程从通道中获取并打印。不要使用 `time.Sleep()` 来同步:[goroutine_panic.go](exercises/chapter_14/goroutine_panic.go) +- b)也许你的方案有效,但可能会引发运行时的 `panic()`:`throw:all goroutines are asleep-deadlock!` 为什么会这样?你如何解决这个问题?[goroutine_close.go](exercises/chapter_14/goroutine_close.go) - c)解决 a)的另外一种方式:使用一个额外的通道传递给协程,然后在结束的时候随便放点什么进去。`main()` 线程检查是否有数据发送给了这个通道,如果有就停止:[goroutine_select.go](exercises/chapter_14/goroutine_select.go) @@ -110,7 +110,7 @@ Received on channel 1: 94348 使用练习 [6.9 fibonacci2.go](exercises/chapter_6/fibonacci2.go) 中的算法写一个更短的 [gofibonacci2.go](exercises/chapter_14/gofibonacci2.go) -使用 `select` 语句来写,并让通道退出([gofibonacci_select.go](exercises/chapter_14/gofibonacci_select.go)) +使用 `select` 语句来写,并让通道退出 ([gofibonacci_select.go](exercises/chapter_14/gofibonacci_select.go)) 注意:当给结果计时并和 6.13 对比时,我们发现使用通道通信的性能开销有轻微削减;这个例子中的算法使用协程并非性能最好的选择;但是 [gofibonacci3](exercises/chapter_14/gofibonacci3.go) 方案使用了 2 个协程带来了 3 倍的提速。 @@ -121,14 +121,14 @@ Received on channel 1: 94348 练习 14.10:[polar_to_cartesian.go](exercises/chapter_14/polar_to_cartesian.go) -(这是一种综合练习,使用到第 4、9、11 章和本章的内容。)写一个可交互的控制台程序,要求用户输入二位平面极坐标上的点(半径和角度(度))。计算对应的笛卡尔坐标系的点的 x 和 y 并输出。使用极坐标和笛卡尔坐标的结构体。 +(这是一种综合练习,使用到第 4、9、11 章和本章的内容。)写一个可交互的控制台程序,要求用户输入二位平面极坐标上的点(半径和角度(度))。计算对应的笛卡尔坐标系的点的 `x` 和 `y` 并输出。使用极坐标和笛卡尔坐标的结构体。 使用通道和协程: - `channel1` 用来接收极坐标 - `channel2` 用来接收笛卡尔坐标 -转换过程需要在协程中进行,从 channel1 中读取然后发送到 channel2。实际上做这种计算不提倡使用协程和通道,但是如果运算量很大很耗时,这种方案设计就非常合适了。 +转换过程需要在协程中进行,从 `channel1` 中读取然后发送到 `channel2`。实际上做这种计算不提倡使用协程和通道,但是如果运算量很大很耗时,这种方案设计就非常合适了。 练习 14.11: [concurrent_pi.go](exercises/chapter_14/concurrent_pi.go) / [concurrent_pi2.go](exercises/chapter_14/concurrent_pi2.go) @@ -140,9 +140,9 @@ Received on channel 1: 94348 再次声明这只是为了一边练习协程的概念一边找点乐子。 -如果你需要的话可使用 `math.pi` 中的 Pi;而且不使用协程会运算的更快。一个急速版本:使用 `GOMAXPROCS`,开启和 `GOMAXPROCS` 同样多个协程。 +如果你需要的话可使用 `math.pi` 中的 `Pi`;而且不使用协程会运算的更快。一个急速版本:使用 `GOMAXPROCS`,开启和 `GOMAXPROCS` 同样多个协程。 -习惯用法:后台服务模式 +**习惯用法:后台服务模式** 服务通常是是用后台协程中的无限循环实现的,在循环中使用 `select` 获取并处理通道中的数据: diff --git a/eBook/14.5.md b/eBook/14.5.md index d794031..08d2bfd 100644 --- a/eBook/14.5.md +++ b/eBook/14.5.md @@ -2,7 +2,7 @@ `time` 包中有一些有趣的功能可以和通道组合使用。 -其中就包含了 `time.Ticker` 结构体,这个对象以指定的时间间隔重复的向通道 C 发送时间值: +其中就包含了 `time.Ticker` 结构体,这个对象以指定的时间间隔重复的向通道 `C` 发送时间值: ```go type Ticker struct { @@ -12,7 +12,7 @@ type Ticker struct { } ``` -时间间隔的单位是 ns(纳秒,int64),在工厂函数 `time.NewTicker` 中以 `Duration` 类型的参数传入:`func NewTicker(dur) *Ticker`。 +时间间隔的单位是 `ns`(纳秒,`int64`),在工厂函数 `time.NewTicker` 中以 `Duration` 类型的参数传入:`func NewTicker(dur) *Ticker`。 在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。 @@ -34,7 +34,7 @@ default: // no value ready to be received } ``` -`time.Tick()` 函数声明为 `Tick(d Duration) <-chan Time`,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 d 为周期给返回的通道发送时间,d 是纳秒数。如果需要像下边的代码一样,限制处理频率(函数 `client.Call()` 是一个 RPC 调用,这里暂不赘述(参见第 [15.9](15.9.md) 节): +`time.Tick()` 函数声明为 `Tick(d Duration) <-chan Time`,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 `d` 为周期给返回的通道发送时间,`d` 是纳秒数。如果需要,像下边的代码一样,可以限制处理频率(函数 `client.Call()` 是一个 RPC 调用,这里暂不赘述(参见第 [15.9](15.9.md) 节)): ```go import "time" @@ -52,7 +52,7 @@ for req := range requests { 问题 14.1:扩展上边的代码,思考如何承载周期请求数的暴增(提示:使用带缓冲通道和计时器对象)。 -定时器(Timer)结构体看上去和计时器(Ticker)结构体的确很像(构造为 `NewTimer(d Duration)`),但是它只发送一次时间,在 `Dration d` 之后。 +定时器 (`Timer`) 结构体看上去和计时器 (`Ticker`) 结构体的确很像(构造为 `NewTimer(d Duration)`),但是它只发送一次时间,在 `Dration d` 之后。 还有 `time.After(d)` 函数,声明如下: @@ -111,7 +111,7 @@ tick. BOOM! ``` -习惯用法:简单超时模式 +**习惯用法:简单超时模式** 要从通道 `ch` 中接收数据,但是最多等待 1 秒。先创建一个信号通道,然后启动一个 `lambda` 协程,协程在给通道发送数据之前是休眠的: @@ -137,7 +137,7 @@ select { 第二种形式:取消耗时很长的同步调用 -也可以使用 `time.After()` 函数替换 `timeout-channel`。可以在 `select` 中通过 `time.After()` 发送的超时信号来停止协程的执行。以下代码,在 `timeoutNs` 纳秒后执行 `select` 的 `timeout` 分支后,执行`client.Call` 的协程也随之结束,不会给通道 `ch` 返回值: +也可以使用 `time.After()` 函数替换 `timeout-channel`。可以在 `select` 中通过 `time.After()` 发送的超时信号来停止协程的执行。以下代码,在 `timeoutNs` 纳秒后执行 `select` 的 `timeout` 分支后,执行 `client.Call` 的协程也随之结束,不会给通道 `ch` 返回值: ```go ch := make(chan error, 1) @@ -151,7 +151,7 @@ case <-time.After(timeoutNs): } ``` -注意缓冲大小设置为 1 是必要的,可以避免协程死锁以及确保超时的通道可以被垃圾回收。此外,需要注意在有多个 `case` 符合条件时, `select` 对 `case` 的选择是伪随机的,如果上面的代码稍作修改如下,则 `select` 语句可能不会在定时器超时信号到来时立刻选中 `time.After(timeoutNs)` 对应的 `case`,因此协程可能不会严格按照定时器设置的时间结束。 +注意缓冲大小设置为 `1` 是必要的,可以避免协程死锁以及确保超时的通道可以被垃圾回收。此外,需要注意在有多个 `case` 符合条件时, `select` 对 `case` 的选择是伪随机的,如果上面的代码稍作修改如下,则 `select` 语句可能不会在定时器超时信号到来时立刻选中 `time.After(timeoutNs)` 对应的 `case`,因此协程可能不会严格按照定时器设置的时间结束。 ```go ch := make(chan int, 1) diff --git a/eBook/14.6.md b/eBook/14.6.md index 1b898d3..458c40e 100644 --- a/eBook/14.6.md +++ b/eBook/14.6.md @@ -1,6 +1,6 @@ -# 14.6 协程和恢复(recover) +# 14.6 协程和恢复 (recover) -一个用到 `recover` 的程序(参见第 13.3 节)停掉了服务器内部一个失败的协程而不影响其他协程的工作。 +一个用到 `recover()` 的程序(参见[第 13.3 节](13.3.md)停掉了服务器内部一个失败的协程而不影响其他协程的工作。 ```go func server(workChan <-chan *Work) { @@ -19,9 +19,9 @@ func safelyDo(work *Work) { } ``` -上边的代码,如果 `do(work)` 发生 panic,错误会被记录且协程会退出并释放,而其他协程不受影响。 +上边的代码,如果 `do(work)` 发生 `panic()`,错误会被记录且协程会退出并释放,而其他协程不受影响。 -因为 `recover` 总是返回 `nil`,除非直接在 `defer` 修饰的函数中调用,`defer` 修饰的代码可以调用那些自身可以使用 `panic` 和 `recover` 避免失败的库例程(库函数)。举例,`safelyDo()` 中 `defer` 修饰的函数可能在调用 `recover` 之前就调用了一个 `logging` 函数,`panicking` 状态不会影响 `logging` 代码的运行。因为加入了恢复模式,函数 `do`(以及它调用的任何东西)可以通过调用 `panic` 来摆脱不好的情况。但是恢复是在 `panicking` 的协程内部的:不能被另外一个协程恢复。 +因为 `recover()` 总是返回 `nil`,除非直接在 `defer` 修饰的函数中调用,`defer` 修饰的代码可以调用那些自身可以使用 `panic()` 和 `recover()` 避免失败的库例程(库函数)。举例,`safelyDo()` 中 `defer` 修饰的函数可能在调用 `recover()` 之前就调用了一个 `logging()` 函数,panicking 状态不会影响 `logging()` 代码的运行。因为加入了恢复模式,函数 `do()`(以及它调用的任何东西)可以通过调用 `panic()` 来摆脱不好的情况。但是恢复是在 panicking 的协程内部的:不能被另外一个协程恢复。 ## 链接 diff --git a/eBook/14.7.md b/eBook/14.7.md index 7334c00..73514a6 100644 --- a/eBook/14.7.md +++ b/eBook/14.7.md @@ -1,4 +1,4 @@ -# 14.7 新旧模型对比:任务和worker +# 14.7 新旧模型对比:任务和 worker 假设我们需要处理很多任务;一个 worker 处理一项任务。任务可以被定义为一个结构体(具体的细节在这里并不重要): @@ -18,7 +18,7 @@ type Task struct { Tasks []*Task } ``` -sync.Mutex([参见9.3](09.3.md))是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个 go 协程(goroutine)可以进入该临界区。如果出现了同一时间多个 go 协程都进入了该临界区,则会产生竞争:Pool 结构就不能保证被正确更新。在传统的模式中(经典的面向对象的语言中应用得比较多,比如 C++,JAVA,C#),worker 代码可能这样写: +`sync.Mutex`([参见9.3](09.3.md))是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个 go 协程 (goroutine) 可以进入该临界区。如果出现了同一时间多个 go 协程都进入了该临界区,则会产生竞争:`Pool` 结构就不能保证被正确更新。在传统的模式中(经典的面向对象的语言中应用得比较多,比如 C++,JAVA,C#),worker 代码可能这样写: ```go func Worker(pool *Pool) { @@ -34,11 +34,11 @@ func Worker(pool *Pool) { } ``` -这些 worker 有许多都可以并发执行;他们可以在 go 协程中启动。一个 worker 先将 pool 锁定,从 pool 获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个 go 协程可以进入到 pool 中:一项任务有且只能被赋予一个 worker 。如果不加锁,则工作协程可能会在 `task:=pool.Tasks[0]` 发生切换,导致 `pool.Tasks=pool.Tasks[1:]` 结果异常:一些 worker 获取不到任务,而一些任务可能被多个 worker 得到。加锁实现同步的方式在工作协程比较少时可以工作得很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。 +这些 worker 有许多都可以并发执行;他们可以在 go 协程中启动。一个 worker 先将 `pool` 锁定,从 `pool` 获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个 go 协程可以进入到 `pool` 中:一项任务有且只能被赋予一个 worker 。如果不加锁,则工作协程可能会在 `task:=pool.Tasks[0]` 发生切换,导致 `pool.Tasks=pool.Tasks[1:]` 结果异常:一些 worker 获取不到任务,而一些任务可能被多个 worker 得到。加锁实现同步的方式在工作协程比较少时可以工作得很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。 新模式:使用通道 -使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker 在协程中启动,其数量 N 应该根据任务数量进行调整。 +使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker 在协程中启动,其数量 `N` 应该根据任务数量进行调整。 主线程扮演着 Master 节点角色,可能写成如下形式: @@ -53,7 +53,7 @@ func Worker(pool *Pool) { } ``` -worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放到done通道中: +worker 的逻辑比较简单:从 `pending` 通道拿任务,处理后将其放到 `done` 通道中: ```go func Worker(in, out chan *Task) { @@ -65,7 +65,7 @@ worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放 } ``` -这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker 数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在 pending 通道中存在一份任务的拷贝,第一个 worker 从 pending 通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的:参见 [14.2.2](14.2.md))。某一个任务会在哪一个 worker 中被执行是不可知的,反过来也是。worker 数量的增多也会增加通信的开销,这会对性能有轻微的影响。 +这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker 数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在 `pending` 通道中存在一份任务的拷贝,第一个 worker 从 `pending` 通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的:参见 [14.2.2](14.2.md))。某一个任务会在哪一个 worker 中被执行是不可知的,反过来也是。worker 数量的增多也会增加通信的开销,这会对性能有轻微的影响。 从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。 @@ -83,11 +83,11 @@ worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放 } ``` -对于任何可以建模为 Master-Worker 范例的问题,一个类似于 worker 使用通道进行通信和交互、Master 进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行 Worker 协程,Master 和 Worker 之间使用 netchan 或者 RPC 进行通信(参见 15 章)。 +对于任何可以建模为 Master-Worker 范例的问题,一个类似于 worker 使用通道进行通信和交互、Master 进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行 Worker 协程,Master 和 Worker 之间使用 netchan 或者 RPC 进行通信(参见 [15 章](15.0.md))。 怎么选择是该使用锁还是通道? -通道是一个较新的概念,本节我们着重强调了在 go 协程里通道的使用,但这并不意味着经典的锁方法就不能使用。go 语言让你可以根据实际问题进行选择:创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁,也不要忌讳使用它。go语言注重实用,什么方式最能解决你的问题就用什么方式,而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则: +通道是一个较新的概念,本节我们着重强调了在 go 协程里通道的使用,但这并不意味着经典的锁方法就不能使用。go 语言让你可以根据实际问题进行选择:创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁,也不要忌讳使用它。go 语言注重实用,什么方式最能解决你的问题就用什么方式,而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则: * 使用锁的情景: - 访问共享数据结构中的缓存信息 @@ -97,7 +97,7 @@ worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放 - 与异步操作的结果进行交互 - 分发任务 - 传递数据所有权 - + 当你发现你的锁使用规则变得很复杂时,可以反省使用通道会不会使问题变得简单些。 ## 链接 diff --git a/eBook/14.8.md b/eBook/14.8.md index d6fc946..9db27a7 100644 --- a/eBook/14.8.md +++ b/eBook/14.8.md @@ -11,7 +11,7 @@ 生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和 CPU):这是一项在需要时对表达式进行求值的技术。例如,生成一个无限数量的偶数序列:要产生这样一个序列并且在一个一个的使用可能会很困难,而且内存会溢出!但是一个含有通道和 go 协程的函数能轻易实现这个需求。 -在 14.12 的例子中,我们实现了一个使用 int 型通道来实现的生成器。通道被命名为 `yield` 和 `resume` ,这些词经常在协程代码中使用。 +在 14.12 的例子中,我们实现了一个使用 `int` 型通道来实现的生成器。通道被命名为 `yield` 和 `resume` ,这些词经常在协程代码中使用。 示例 14.12 [lazy_evaluation.go](examples/chapter_14/lazy_evaluation.go): @@ -50,7 +50,7 @@ func main() { 有一个细微的区别是从通道读取的值可能会是稍早前产生的,并不是在程序被调用时生成的。如果确实需要这样的行为,就得实现一个请求响应机制。当生成器生成数据的过程是计算密集型且各个结果的顺序并不重要时,那么就可以将生成器放入到 go 协程实现并行化。但是得小心,使用大量的 go 协程的开销可能会超过带来的性能增益。 -这些原则可以概括为:通过巧妙地使用空接口、闭包和高阶函数,我们能实现一个通用的惰性生产器的工厂函数 `BuildLazyEvaluator`(这个应该放在一个工具包中实现)。工厂函数需要一个函数和一个初始状态作为输入参数,返回一个无参、返回值是生成序列的函数。传入的函数需要计算出下一个返回值以及下一个状态参数。在工厂函数中,创建一个通道和无限循环的 go 协程。返回值被放到了该通道中,返回函数稍后被调用时从该通道中取得该返回值。每当取得一个值时,下一个值即被计算。在下面的例子中,定义了一个 `evenFunc` 函数,其是一个惰性生成函数:在 main 函数中,我们创建了前 10 个偶数,每个都是通过调用 `even()` 函数取得下一个值的。为此,我们需要在 `BuildLazyIntEvaluator` 函数中具体化我们的生成函数,然后我们能够基于此做出定义。 +这些原则可以概括为:通过巧妙地使用空接口、闭包和高阶函数,我们能实现一个通用的惰性生产器的工厂函数 `BuildLazyEvaluator`(这个应该放在一个工具包中实现)。工厂函数需要一个函数和一个初始状态作为输入参数,返回一个无参、返回值是生成序列的函数。传入的函数需要计算出下一个返回值以及下一个状态参数。在工厂函数中,创建一个通道和无限循环的 go 协程。返回值被放到了该通道中,返回函数稍后被调用时从该通道中取得该返回值。每当取得一个值时,下一个值即被计算。在下面的例子中,定义了一个 `evenFunc` 函数,其是一个惰性生成函数:在 `main()` 函数中,我们创建了前 10 个偶数,每个都是通过调用 `even()` 函数取得下一个值的。为此,我们需要在 `BuildLazyIntEvaluator` 函数中具体化我们的生成函数,然后我们能够基于此做出定义。 示例 14.13 [general_lazy_evalution1.go](examples/chapter_14/general_lazy_evalution1.go): diff --git a/eBook/14.9.md b/eBook/14.9.md index c18a72d..cb62a87 100644 --- a/eBook/14.9.md +++ b/eBook/14.9.md @@ -14,7 +14,7 @@ func InverseProduct(a Matrix, b Matrix) { } ``` -在这个例子中,a 和 b 的求逆矩阵需要先被计算。那么为什么在计算 b 的逆矩阵时,需要等待 a 的逆计算完成呢?显然不必要,这两个求逆运算其实可以并行执行的。换句话说,调用 `Product` 函数只需要等到 `a_inv` 和 `b_inv` 的计算完成。如下代码实现了并行计算方式: +在这个例子中,`a` 和 `b` 的求逆矩阵需要先被计算。那么为什么在计算 `b` 的逆矩阵时,需要等待 `a` 的逆计算完成呢?显然不必要,这两个求逆运算其实可以并行执行的。换句话说,调用 `Product()` 函数只需要等到 `a_inv` 和 `b_inv` 的计算完成。如下代码实现了并行计算方式: ```go func InverseProduct(a Matrix, b Matrix) { @@ -26,7 +26,7 @@ func InverseProduct(a Matrix, b Matrix) { } ``` -`InverseFuture` 函数以 `goroutine` 的形式起了一个闭包,该闭包会将矩阵求逆结果放入到 future 通道中: +`InverseFuture()` 函数以 `goroutine` 的形式起了一个闭包,该闭包会将矩阵求逆结果放入到 `future` 通道中: ```go func InverseFuture(a Matrix) chan Matrix {