This commit is contained in:
Unknwon
2016-01-04 06:29:34 +08:00
parent 3cb960fd21
commit 5e06368e45
3 changed files with 133 additions and 74 deletions

View File

@@ -9,7 +9,7 @@
## 翻译进度 ## 翻译进度
14.1 [并发、并行和协程](eBook/14.1.md) 14.2 [使用通道进行协程间通信](eBook/14.2.md)
## 支持本书 ## 支持本书

View File

@@ -31,4 +31,4 @@ Golang 编程245386165
|更新日期 |更新内容 |更新日期 |更新内容
|----------|------------------ |----------|------------------
|2015-12-31|14.1 并发、并行和协程 |2015-1-4|14.2 使用通道进行协程间通信

View File

@@ -19,15 +19,17 @@
所以通道稚嫩传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以。甚至可以(有时非常有用)创建通道的通道。 所以通道稚嫩传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以。甚至可以(有时非常有用)创建通道的通道。
通道实际上是类型化消息的队列使数据得以传输。它是先进先出FIFO结构的所以可以保证发送给他们的元素的顺序有些人知道通道可以比作 Unix shells 中的双向管道tw-way pipe。通道也是引用类型所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1然后创建了它实例化 通道实际上是类型化消息的队列使数据得以传输。它是先进先出FIFO结构的所以可以保证发送给他们的元素的顺序有些人知道通道可以比作 Unix shells 中的双向管道tw-way pipe。通道也是引用类型所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1然后创建了它实例化
```go ```go
var ch1 chan string var ch1 chan string
ch1 = make(chan string) ch1 = make(chan string)
``` ```
当然可以更短: `ch1 := make(chan string)`
这里我们构建一个int通道的通道 `chanOfChans := make(chan chan int)` 当然可以更短: `ch1 := make(chan string)`
或者函数通道: `funcChan := chan func()`(相关示例请看章节[14.17](14.17.md) 这里我们构建一个int通道的通道 `chanOfChans := make(chan chan int)`
或者函数通道:`funcChan := chan func()`(相关示例请看第 [14.17](14.17.md) 节)。
所以通道是对象的第一类型:可以存储在变量中,作为函数的参数传递,从函数返回以及通过通道发送它们自身。另外它们是类型化的,允许类型检查,比如尝试使用整数通道发送一个指针。 所以通道是对象的第一类型:可以存储在变量中,作为函数的参数传递,从函数返回以及通过通道发送它们自身。另外它们是类型化的,允许类型检查,比如尝试使用整数通道发送一个指针。
@@ -41,17 +43,20 @@ ch1 = make(chan string)
从通道流出(接收),三种方式: 从通道流出(接收),三种方式:
`int2 = <- ch`表示变量int2从通道ch一元运算的前缀操作符前缀 = 接收接收数据获取新值假设int2已经声明过了如果没有的话可以写成`int2 := <- ch` `int2 = <- ch` 表示:变量 int2 从通道 ch一元运算的前缀操作符前缀 = 接收)接收数据(获取新值);假设 int2 已经声明过了,如果没有的话可以写成:`int2 := <- ch`
`<- ch` 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的: `<- ch` 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的:
```go ```go
if <- ch != 1000{ if <- ch != 1000{
... ...
} }
``` ```
操作符 <- 也被用来发送和接收Go 尽管不必要为了可读性通道的命名通常以 `ch` 开头或者包含 `chan`通道的发送和接收操作都是自动的它们通常一气呵成下面的示例展示了通信操作 操作符 <- 也被用来发送和接收Go 尽管不必要为了可读性通道的命名通常以 `ch` 开头或者包含 `chan`通道的发送和接收操作都是自动的它们通常一气呵成下面的示例展示了通信操作
示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go) 示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go)
```go ```go
package main package main
@@ -86,10 +91,13 @@ func getData(ch chan string) {
} }
} }
``` ```
输出 输出
``` ```
Washington Tripoli London Beijing Tokio Washington Tripoli London Beijing Tokio
``` ```
`main()` 函数中启动了两个协程`sendData()` 通过通道 ch 发送了 5 个字符串`getData()` 按顺序接收它们并打印出来 `main()` 函数中启动了两个协程`sendData()` 通过通道 ch 发送了 5 个字符串`getData()` 按顺序接收它们并打印出来
如果 2 个协程需要通信你必须给他们同一个通道作为参数才行 如果 2 个协程需要通信你必须给他们同一个通道作为参数才行
@@ -97,12 +105,15 @@ Washington Tripoli London Beijing Tokio
尝试一下如果注释掉 `time.Sleep(1e9)` 会如何 尝试一下如果注释掉 `time.Sleep(1e9)` 会如何
我们发现协程之间的同步非常重要 我们发现协程之间的同步非常重要
* main()等待了1秒让两个协程完成如果不这样sendData()就没有机会输出
* getData()使用了无限循环它随着sendData()的发送完成和ch变空也结束了 - main() 等待了 1 秒让两个协程完成如果不这样sendData() 就没有机会输出
* 如果我们移除一个或所有go关键字程序无法运行Go运行时会抛出panic - 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! ---- 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!
``` ```
为什么会这样运行时会检查所有的协程也许只有一个是这种情况是否在等待可以读取或者写入某个通道意味着程序无法处理这是死锁deadlock形式运行时可以检测到这种情况 为什么会这样运行时会检查所有的协程也许只有一个是这种情况是否在等待可以读取或者写入某个通道意味着程序无法处理这是死锁deadlock形式运行时可以检测到这种情况
注意不要使用打印状态来表明通道的发送和接收顺序由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同 注意不要使用打印状态来表明通道的发送和接收顺序由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同
@@ -122,6 +133,7 @@ Washington Tripoli London Beijing Tokio
程序 `channel_block.go` 验证了以上理论一个协程在无限循环中给通道发送整数数据不过因为没有接收者只输出了一个数字 0 程序 `channel_block.go` 验证了以上理论一个协程在无限循环中给通道发送整数数据不过因为没有接收者只输出了一个数字 0
示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go) 示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go)
```go ```go
package main package main
@@ -139,13 +151,17 @@ func pump(ch chan int) {
} }
} }
``` ```
输出 输出
``` ```
0 0
``` ```
`pump()` 函数为通道提供数值也被叫做生产者 `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 ```go
func suck(ch chan int) { func suck(ch chan int) {
for { for {
@@ -153,12 +169,15 @@ func suck(ch chan int) {
} }
} }
``` ```
`main()` 中使用协程开始它 `main()` 中使用协程开始它
```go ```go
go pump(ch1) go pump(ch1)
go suck(ch1) go suck(ch1)
time.Sleep(1e9) time.Sleep(1e9)
``` ```
给程序 1 秒的时间来运行输出了上万个整数 给程序 1 秒的时间来运行输出了上万个整数
练习 14.1[channel_block3.go](exercises/chapter_14/channel_block3.go)写一个通道证明它的阻塞性开启一个协程接收通道的数据持续 15 然后给通道放入一个值在不同的阶段打印消息并观察输出 练习 14.1[channel_block3.go](exercises/chapter_14/channel_block3.go)写一个通道证明它的阻塞性开启一个协程接收通道的数据持续 15 然后给通道放入一个值在不同的阶段打印消息并观察输出
@@ -174,6 +193,7 @@ time.Sleep(1e9)
练习 14.2 [blocking.go](exercises/chapter_14/blocking.go) 练习 14.2 [blocking.go](exercises/chapter_14/blocking.go)
解释为什么下边这个程序会导致 panic所有的协程都休眠了 - 死锁 解释为什么下边这个程序会导致 panic所有的协程都休眠了 - 死锁
```go ```go
package main package main
@@ -195,11 +215,13 @@ func main() {
## 14.2.5 同步通道-使用带缓冲的通道 ## 14.2.5 同步通道-使用带缓冲的通道
一个无缓冲通道只能包含 1 个元素有时显得很局限我们给通道提供了一个缓存可以在扩展的 `make` 命令中设置它的容量如下 一个无缓冲通道只能包含 1 个元素有时显得很局限我们给通道提供了一个缓存可以在扩展的 `make` 命令中设置它的容量如下
```go ```go
buf := 100 buf := 100
ch1 := make(chan string, buf) ch1 := make(chan string, buf)
``` ```
buf是通道可以承受的元素这里是string个数
buf 是通道可以同时容纳的元素这里是 string个数
在缓冲满载缓冲被全部使用之前给一个带缓冲的通道发送数据是不会阻塞的而从通道读取数据也不会阻塞直到缓冲空了 在缓冲满载缓冲被全部使用之前给一个带缓冲的通道发送数据是不会阻塞的而从通道读取数据也不会阻塞直到缓冲空了
@@ -207,11 +229,10 @@ buf是通道可以承受的元素这里是string个数
如果容量大于 0通道就是异步的了缓冲满载发送或变空接收之前通信不会阻塞元素会按照发送的顺序被接收如果容量是0或者未设置通信仅在收发双方准备好的情况下才可以成功 如果容量大于 0通道就是异步的了缓冲满载发送或变空接收之前通信不会阻塞元素会按照发送的顺序被接收如果容量是0或者未设置通信仅在收发双方准备好的情况下才可以成功
同步ch :=make(chan type, value) 同步`ch :=make(chan type, value)`
value == 0 -> synchronous, unbuffered (阻塞) - value == 0 -> synchronous, unbuffered (阻塞)
- value > 0 -> asynchronous, buffered非阻塞取决于value元素
value > 0 -> asynchronous, buffered非阻塞取决于value元素
若使用通道的缓冲你的程序会在“请求”激增的时候表现更好更具弹性专业术语叫更具有伸缩性scalable。要在首要位置使用无缓冲通道来设计算法只在不确定的情况下使用缓冲。 若使用通道的缓冲你的程序会在“请求”激增的时候表现更好更具弹性专业术语叫更具有伸缩性scalable。要在首要位置使用无缓冲通道来设计算法只在不确定的情况下使用缓冲。
@@ -220,12 +241,14 @@ value > 0 -> asynchronous, buffered非阻塞取决于value元素
## 14.2.6 协程中用通道输出结果 ## 14.2.6 协程中用通道输出结果
为了知道计算何时完成,可以通过信道回报。在例子 `go sum(bigArray)` 中,要这样写: 为了知道计算何时完成,可以通过信道回报。在例子 `go sum(bigArray)` 中,要这样写:
```go ```go
ch := make(chan int) ch := make(chan int)
go sum(bigArray, ch) // bigArray puts the calculated sum on ch go sum(bigArray, ch) // bigArray puts the calculated sum on ch
// .. do something else for a while // .. do something else for a while
sum := <- ch // wait for, and retrieve the sum sum := <- ch // wait for, and retrieve the sum
``` ```
也可以使用通道来达到同步的目的这个很有效的用法在传统计算机中成为semaphore。或者换个方式通过通道发送信号告知处理已经完成在协程中 也可以使用通道来达到同步的目的这个很有效的用法在传统计算机中成为semaphore。或者换个方式通过通道发送信号告知处理已经完成在协程中
在其他协程运行时让 main 程序无限阻塞的通常做法是在 `main` 函数的最后放置一个{}。 在其他协程运行时让 main 程序无限阻塞的通常做法是在 `main` 函数的最后放置一个{}。
@@ -237,6 +260,7 @@ sum := <- ch // wait for, and retrieve the sum
下边的片段阐明:协程通过在通道 `ch` 中放置一个值来处理结束的信号。`main` 协程等待 `<-ch` 直到从中获取到值。 下边的片段阐明:协程通过在通道 `ch` 中放置一个值来处理结束的信号。`main` 协程等待 `<-ch` 直到从中获取到值。
我们期望从这个通道中获取返回的结果,像这样: 我们期望从这个通道中获取返回的结果,像这样:
```go ```go
func compute(ch chan int){ func compute(ch chan int){
ch <- someComputation() // when it completes, signal on the channel. ch <- someComputation() // when it completes, signal on the channel.
@@ -249,8 +273,9 @@ func main(){
result := <- ch result := <- ch
} }
``` ```
这个信号也可以是其他的不反回结果比如下边这个协程中的lambda函数
协程: 这个信号也可以是其他的,不反回结果,比如下边这个协程中的 lambda 函数协程:
```go ```go
ch := make(chan int) ch := make(chan int)
go func(){ go func(){
@@ -260,7 +285,9 @@ go func(){
doSomethingElseForAWhile() doSomethingElseForAWhile()
<- ch // Wait for goroutine to finish; discard sent value. <- ch // Wait for goroutine to finish; discard sent value.
``` ```
或者等待两个协程完成每一个都会对切片s的一部分进行排序片段如下 或者等待两个协程完成每一个都会对切片s的一部分进行排序片段如下
```go ```go
done := make(chan bool) done := make(chan bool)
// doSort is a lambda function, so a closure which knows the channel done: // doSort is a lambda function, so a closure which knows the channel done:
@@ -274,7 +301,9 @@ go doSort(s[i:])
<-done <-done
<-done <-done
``` ```
下边的代码,用完整的信号量模式对 size 长度的 gloat64 切片进行了 N 个` doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(切包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。 下边的代码,用完整的信号量模式对 size 长度的 gloat64 切片进行了 N 个` doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(切包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。
```go ```go
type Empty interface {} type Empty interface {}
var empty Empty var empty Empty
@@ -292,11 +321,13 @@ for i, xi := range data {
// wait for goroutines to finish // wait for goroutines to finish
for i := 0; i < N; i++ { <-sem } for i := 0; i < N; i++ { <-sem }
``` ```
注意闭合:`i``xi`都是作为参数传入闭合函数的,从外层循环中隐藏了变量`i``xi`。让每个协程有一份`i``xi`的拷贝另外for循环的下一次迭代会更新所有协程中`i``xi`的值。切片`res`没有传入闭合函数,因为协程不需要单独拷贝一份。切片`res`也在闭合函数中但并不是参数。
注意闭合:`i``xi` 都是作为参数传入闭合函数的,从外层循环中隐藏了变量 `i``xi`。让每个协程有一份 `i``xi` 的拷贝另外for 循环的下一次迭代会更新所有协程中 `i``xi` 的值。切片 `res` 没有传入闭合函数,因为协程不需要单独拷贝一份。切片 `res` 也在闭合函数中但并不是参数。
## 14.2.8 实现并行的 for 循环 ## 14.2.8 实现并行的 for 循环
在上一部分章节 [14.2.7](14.2.7.md) 的代码片段中for 循环的每一个迭代是并行完成的: 在上一部分章节 [14.2.7](14.2.7.md) 的代码片段中for 循环的每一个迭代是并行完成的:
```go ```go
for i, v := range data { for i, v := range data {
go func (i int, v float64) { go func (i int, v float64) {
@@ -305,24 +336,28 @@ for i, v := range data {
} (i, v) } (i, v)
} }
``` ```
在 for 循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如 Fortress 或者其他并行框架以不同的结构实现了这种方式,在 Go 中用协程实现起来非常容易: 在 for 循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如 Fortress 或者其他并行框架以不同的结构实现了这种方式,在 Go 中用协程实现起来非常容易:
## 14.2.9 用带缓冲通道实现一个信号量 ## 14.2.9 用带缓冲通道实现一个信号量
信号量是实现互斥锁(排外锁)常见的同步机制,限制对资源的访问,解决读写问题,比如没有实现信号量的 `sync` 的 Go 包,使用带缓冲的通道可以轻松实现: 信号量是实现互斥锁(排外锁)常见的同步机制,限制对资源的访问,解决读写问题,比如没有实现信号量的 `sync` 的 Go 包,使用带缓冲的通道可以轻松实现:
* 带缓冲通道的容量和我们要同步的资源容量相同 - 带缓冲通道的容量和我们要同步的资源容量相同
* 通道的长度(当前存放的元素个数)当前资源被使用的数量相同 - 通道的长度(当前存放的元素个数)当前资源被使用的数量相同
* 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值) - 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
不用管通道中存放的是什么,只关注长度;因此我们创建了一个有长度变量为 0字节的通道 不用管通道中存放的是什么,只关注长度;因此我们创建了一个有长度变量为 0字节的通道
```go ```go
type Empty interface {} type Empty interface {}
type semaphore chan Empty type semaphore chan Empty
``` ```
将可用资源的数量N来初始化信号量 `semaphore``sem = make(semaphore, N)` 将可用资源的数量N来初始化信号量 `semaphore``sem = make(semaphore, N)`
然后直接对信号量进行操作: 然后直接对信号量进行操作:
```go ```go
// acquire n resources // acquire n resources
func (s semaphore) P(n int) { func (s semaphore) P(n int) {
@@ -339,7 +374,9 @@ func (s semaphore) V(n int) {
} }
} }
``` ```
可以用来实现一个互斥的例子: 可以用来实现一个互斥的例子:
```go ```go
/* mutexes */ /* mutexes */
func (s semaphore) Lock() { func (s semaphore) Lock() {
@@ -359,6 +396,7 @@ func (s semaphore) Signal() {
s.V(1) s.V(1)
} }
``` ```
练习 14.5[gosum.go](exercises/chapter_14/gosum.go)用这种习惯用法写一个程序开启一个协程来计算2个整数的合并等待计算结果并打印出来。 练习 14.5[gosum.go](exercises/chapter_14/gosum.go)用这种习惯用法写一个程序开启一个协程来计算2个整数的合并等待计算结果并打印出来。
练习 14.6[producer_consumer.go](exercises/chapter_14/producer_consumer.go):用这种习惯用法写一个程序,有两个协程,第一个提供数字 01020...90 并将他们放入通道,第二个协程从通道中读取并打印。`main()` 等待两个协程完成后再结束。 练习 14.6[producer_consumer.go](exercises/chapter_14/producer_consumer.go):用这种习惯用法写一个程序,有两个协程,第一个提供数字 01020...90 并将他们放入通道,第二个协程从通道中读取并打印。`main()` 等待两个协程完成后再结束。
@@ -368,6 +406,7 @@ func (s semaphore) Signal() {
编程中常见另外一种模式如下:不将通道作为参数传递给协程,而用函数来生成一个通道并返回(工厂角色);函数内有个 lambda 函数被协程调用。 编程中常见另外一种模式如下:不将通道作为参数传递给协程,而用函数来生成一个通道并返回(工厂角色);函数内有个 lambda 函数被协程调用。
在 [channel_block2.go](examples/chapter_14/channel_block2.go) 加入这种模式便有了示例 14.5-[channel_idiom.go](examples/chapter_14/channel_idiom.go) 在 [channel_block2.go](examples/chapter_14/channel_block2.go) 加入这种模式便有了示例 14.5-[channel_idiom.go](examples/chapter_14/channel_idiom.go)
```go ```go
package main package main
@@ -399,17 +438,20 @@ func suck(ch chan int) {
} }
``` ```
## 14.2.10 给通道使用For循环 ## 14.2.10 给通道使用 for 循环
`for` 循环的 `range` 语句可以用在通道 `ch` 上,便可以从通道中获取值,像这样: `for` 循环的 `range` 语句可以用在通道 `ch` 上,便可以从通道中获取值,像这样:
```go ```go
for v := range ch { for v := range ch {
fmt.Printf("The value is %v\n", v) fmt.Printf("The value is %v\n", v)
} }
``` ```
它从指定通道中读取数据直到通道关闭,才继续执行下边的代码。很明显,另外一个协程必须写入 `ch`(不然代码就阻塞在 for 循环了),而且必须在写入完成后才关闭。`suck` 函数可以这样写,且在协程中调用这个动作,程序变成了这样: 它从指定通道中读取数据直到通道关闭,才继续执行下边的代码。很明显,另外一个协程必须写入 `ch`(不然代码就阻塞在 for 循环了),而且必须在写入完成后才关闭。`suck` 函数可以这样写,且在协程中调用这个动作,程序变成了这样:
示例 14.6-[channel_idiom2.go](examples/chapter_14/channel_idiom2.go) 示例 14.6-[channel_idiom2.go](examples/chapter_14/channel_idiom2.go)
```go ```go
package main package main
@@ -444,7 +486,8 @@ func suck(ch chan int) {
习惯用法:通道迭代模式 习惯用法:通道迭代模式
这个模式用到了前边示例[14.6](exercises/chapter_14/producer_consumer.go)中的模式通常需要从包含了地址索引字段items的容器给通道填入元素。为容器的类型定义一个方法`Iter()`,返回一个只读的通道(参见章节[14.2.8](14.2.8.md)items如下 这个模式用到了前边示例 [14.6](exercises/chapter_14/producer_consumer.go) 中的模式,通常,需要从包含了地址索引字段 items 的容器给通道填入元素。为容器的类型定义一个方法 `Iter()`,返回一个只读的通道(参见[14.2.8](14.2.8.md)items如下
```go ```go
func (c *container) Iter () <- chan items { func (c *container) Iter () <- chan items {
ch := make(chan item) ch := make(chan item)
@@ -456,17 +499,21 @@ func (c *container) Iter () <- chan items {
return ch return ch
} }
``` ```
在协程里,一个 for 循环迭代容器 c 中的元素(对于树或图的算法,这种简单的 for 循环可以替换为深度优先搜索)。 在协程里,一个 for 循环迭代容器 c 中的元素(对于树或图的算法,这种简单的 for 循环可以替换为深度优先搜索)。
调用这个方法的代码可以这样迭代容器: 调用这个方法的代码可以这样迭代容器:
```go ```go
for x := range container.Iter() { ... } for x := range container.Iter() { ... }
``` ```
可以运行在自己的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在两个线程上)。就有了一个特殊的生产者-消费者模式。如果程序在协程给通道写完值之前结束,协程不会被回收;设计如此。这种行为看起来是错误的,但是通道是一种线程安全的通信。在这种情况下,协程尝试写入一个通道,而这个通道永远不会被读取,这可能是个 bug 而并非期望它被静默的回收。 可以运行在自己的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在两个线程上)。就有了一个特殊的生产者-消费者模式。如果程序在协程给通道写完值之前结束,协程不会被回收;设计如此。这种行为看起来是错误的,但是通道是一种线程安全的通信。在这种情况下,协程尝试写入一个通道,而这个通道永远不会被读取,这可能是个 bug 而并非期望它被静默的回收。
习惯用法:生产者消费者模式 习惯用法:生产者消费者模式
假设你有 `Produce()` 函数来产生 `Consume` 函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环: 假设你有 `Produce()` 函数来产生 `Consume` 函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环:
```go ```go
for { for {
Consume(Produce()) Consume(Produce())
@@ -476,11 +523,14 @@ for {
## 14.2.11 通道的方向 ## 14.2.11 通道的方向
通道类型可以用注解来表示它只发送或者只接收: 通道类型可以用注解来表示它只发送或者只接收:
```go ```go
var send_only chan<- int // channel can only receive data var send_only chan<- int // channel can only receive data
var recv_only <-chan int // channel can onley send data var recv_only <-chan int // channel can onley send data
``` ```
只接收的通道(<-chan T无法关闭因为关闭通道是发送者用来表示不再给通道发送值了所以对只接收通道是没有意义的通道创建的时候都是双向的但也可以分配有方向的通道变量就像以下代码 只接收的通道(<-chan T无法关闭因为关闭通道是发送者用来表示不再给通道发送值了所以对只接收通道是没有意义的通道创建的时候都是双向的但也可以分配有方向的通道变量就像以下代码
```go ```go
var c = make(chan int) // bidirectional var c = make(chan int) // bidirectional
go source(c) go source(c)
@@ -498,6 +548,7 @@ func sink(ch <-chan int) {
习惯用法管道和选择器模式 习惯用法管道和选择器模式
更具体的例子还有协程处理它从通道接收的数据并发送给输出通道 更具体的例子还有协程处理它从通道接收的数据并发送给输出通道
```go ```go
sendChan := make(chan int) sendChan := make(chan int)
reciveChan := make(chan string) reciveChan := make(chan string)
@@ -510,6 +561,7 @@ func processChannel(in <-chan int, out chan<- string) {
} }
} }
``` ```
通过使用方向注解来限制协程对通道的操作 通过使用方向注解来限制协程对通道的操作
这里有一个来自 Go 指导的很赞的例子打印了输出的素数使用选择器作为它的算法每个 prime 都有一个选择器如下图 这里有一个来自 Go 指导的很赞的例子打印了输出的素数使用选择器作为它的算法每个 prime 都有一个选择器如下图
@@ -517,6 +569,7 @@ func processChannel(in <-chan int, out chan<- string) {
![](../images/14.2_fig14.2.png?raw=true) ![](../images/14.2_fig14.2.png?raw=true)
版本1示例 14.7-[sieve1.go](examples/chapter_14/sieve1.go) 版本1示例 14.7-[sieve1.go](examples/chapter_14/sieve1.go)
```go ```go
// Copyright 2009 The Go Authors. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
@@ -556,9 +609,13 @@ func main() {
} }
} }
``` ```
协程 `filter(in, out chan int, prime int)` 拷贝整数到输出通道丢弃掉可以被 prime 整除的数字然后每个 prime 又开启了一个新的协程生成器和选择器并发请求 协程 `filter(in, out chan int, prime int)` 拷贝整数到输出通道丢弃掉可以被 prime 整除的数字然后每个 prime 又开启了一个新的协程生成器和选择器并发请求
输出
``` ```
输出:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101
103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223
227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349
353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479
@@ -567,9 +624,11 @@ func main() {
773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929
937 941 947 953 967 971 977 983 991 997 1009 1013... 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) 版本2示例 14.8-[sieve2.go](examples/chapter_14/sieve2.go)
```go ```go
// Copyright 2009 The Go Authors. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
@@ -629,5 +688,5 @@ func main() {
## 链接 ## 链接
- [目录](directory.md) - [目录](directory.md)
- 上一节[并发并行和协程](14.1.md) - 上一节[并发并行和协程](14.1.md)
- 下一节[协程同步:关闭通道-测试阻塞的通道](14.3.md) - 下一节[协程同步:关闭通道-测试阻塞的通道](14.3.md)