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

@@ -16,18 +16,20 @@
未初始化的通道的值是nil。 未初始化的通道的值是nil。
所以通道稚嫩传输一种类型的数据,比如`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) 节)。
所以通道是对象的第一类型:可以存储在变量中,作为函数的参数传递,从函数返回以及通过通道发送它们自身。另外它们是类型化的,允许类型检查,比如尝试使用整数通道发送一个指针。 所以通道是对象的第一类型:可以存储在变量中,作为函数的参数传递,从函数返回以及通过通道发送它们自身。另外它们是类型化的,允许类型检查,比如尝试使用整数通道发送一个指针。
@@ -37,21 +39,24 @@ 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` 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的:
`<- 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,42 +91,49 @@ func getData(ch chan string) {
} }
} }
``` ```
输出 输出
``` ```
Washington Tripoli London Beijing Tokio Washington Tripoli London Beijing Tokio
``` ```
`main()`函数中启动了两个协程`sendData()`通过通道ch发送了5个字符串`getData()`按顺序接收它们并打印出来
如果2个协程需要通信你必须给他们同一个通道作为参数才行 `main()` 函数中启动了两个协程`sendData()` 通过通道 ch 发送了 5 个字符串`getData()` 按顺序接收它们并打印出来
尝试一下如果注释掉`time.Sleep(1e9)`会如何 如果 2 个协程需要通信你必须给他们同一个通道作为参数才行
尝试一下如果注释掉 `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形式运行时可以检测到这种情况
注意不要使用打印状态来表明通道的发送和接收顺序由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同 注意不要使用打印状态来表明通道的发送和接收顺序由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同
练习 14.4解释一下为什么如果在函数`getData()`的一开始插入`time.Sleep(1e9)`不会出现错误但也没有输出呢 练习 14.4解释一下为什么如果在函数 `getData()` 的一开始插入 `time.Sleep(1e9)`不会出现错误但也没有输出呢
## 14.2.3 通道阻塞 ## 14.2.3 通道阻塞
默认情况下通信是同步且无缓冲的在有接受者接收数据之前发送不会结束可以想象一个无缓冲的通道在没有空间来保存数据的时候必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者所以通道的发送/接收操作在对方准备好之前是阻塞的 默认情况下通信是同步且无缓冲的在有接受者接收数据之前发送不会结束可以想象一个无缓冲的通道在没有空间来保存数据的时候必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者所以通道的发送/接收操作在对方准备好之前是阻塞的
1对于同一个通道发送操作协程或者函数中的在接收者准备好之前是阻塞的如果ch中的数据无人接收就无法再给通道传入其他数据新的输入无法在通道非空的情况下传入所以发送操作会等待ch再次变为可用状态就是通道值被接收时可以传入变量)。 1对于同一个通道发送操作协程或者函数中的在接收者准备好之前是阻塞的如果ch中的数据无人接收就无法再给通道传入其他数据新的输入无法在通道非空的情况下传入所以发送操作会等待 ch 再次变为可用状态就是通道值被接收时可以传入变量)。
2对于同一个通道接收操作是阻塞的协程或函数中的直到发送者可用如果通道中没有数据接收者就阻塞了 2对于同一个通道接收操作是阻塞的协程或函数中的直到发送者可用如果通道中没有数据接收者就阻塞了
尽管这看上去是非常严格的约束实际在大部分情况下工作的很不错 尽管这看上去是非常严格的约束实际在大部分情况下工作的很不错
程序`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()`函数为通道提供数值也被叫做生产者
为通道解除阻塞定义了`suck`函数来在无限循环中读取通道参见示例 14.4-[channel_block2.go](examples/chapter_14/channel_block2.go) `pump()` 函数为通道提供数值也被叫做生产者
为通道解除阻塞定义了 `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,27 +169,31 @@ 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秒的时间来运行输出了上万个整数
练习 14.1[channel_block3.go](exercises/chapter_14/channel_block3.go)写一个通道证明它的阻塞性开启一个协程接收通道的数据持续15秒然后给通道放入一个值在不同的阶段打印消息并观察输出 给程序 1 秒的时间来运行输出了上万个整数
练习 14.1[channel_block3.go](exercises/chapter_14/channel_block3.go)写一个通道证明它的阻塞性开启一个协程接收通道的数据持续 15 然后给通道放入一个值在不同的阶段打印消息并观察输出
## 14.2.4 通过一个(或多个)通道交换数据进行协程同步。 ## 14.2.4 通过一个(或多个)通道交换数据进行协程同步。
通信是一种同步形式通过通道两个协程在通信协程会和中某刻同步交换数据无缓冲通道成为了多个协程同步的完美工具 通信是一种同步形式通过通道两个协程在通信协程会和中某刻同步交换数据无缓冲通道成为了多个协程同步的完美工具
甚至可以在通道两端互相阻塞对方形成了叫做死锁的状态Go运行时会检查并panic停止程序死锁几乎完全是由糟糕的设计导致的 甚至可以在通道两端互相阻塞对方形成了叫做死锁的状态Go 运行时会检查并 panic停止程序死锁几乎完全是由糟糕的设计导致的
无缓冲通道会被阻塞设计无阻塞的程序可以避免这种情况或者使用带缓冲的通道 无缓冲通道会被阻塞设计无阻塞的程序可以避免这种情况或者使用带缓冲的通道
练习 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
@@ -194,49 +214,53 @@ 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个数
在缓冲满载缓冲被全部使用之前给一个带缓冲的通道发送数据是不会阻塞的而从通道读取数据也不会阻塞直到缓冲空了 在缓冲满载缓冲被全部使用之前给一个带缓冲的通道发送数据是不会阻塞的而从通道读取数据也不会阻塞直到缓冲空了
缓冲容量和类型无关所以可以尽管可能导致危险给一些通道设置不同的容量只要他们拥有同样的元素类型内置的`cap`函数可以返回缓冲区的容量 缓冲容量和类型无关所以可以尽管可能导致危险给一些通道设置不同的容量只要他们拥有同样的元素类型内置的 `cap` 函数可以返回缓冲区的容量
如果容量大于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。要在首要位置使用无缓冲通道来设计算法只在不确定的情况下使用缓冲。
练习 14.3[channel_buffer.go](exercises/chapter_14/channel_buffer.go):给[channel_block3.go](exercises/chapter_14/channel_block3.go)的通道增加缓冲并观察输出有何不同。 练习 14.3[channel_buffer.go](exercises/chapter_14/channel_buffer.go):给 [channel_block3.go](exercises/chapter_14/channel_block3.go) 的通道增加缓冲并观察输出有何不同。
## 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` 函数的最后放置一个{}。
也可以使用通道让`main`程序等待协程完成,就是所谓的信号量模式,我们会在接下来的部分讨论。 也可以使用通道让 `main` 程序等待协程完成,就是所谓的信号量模式,我们会在接下来的部分讨论。
## 14.2.7 信号量模式 ## 14.2.7 信号量模式
下边的片段阐明:协程通过在通道`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`也在闭合函数中但并不是参数。
## 14.2.8 实现并行的for循环 注意闭合:`i``xi` 都是作为参数传入闭合函数的,从外层循环中隐藏了变量 `i``xi`。让每个协程有一份 `i``xi` 的拷贝;另外,for 循环的下一次迭代会更新所有协程中 `i``xi` 的值。切片 `res` 没有传入闭合函数,因为协程不需要单独拷贝一份。切片 `res` 也在闭合函数中但并不是参数。
## 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,15 +396,17 @@ 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()` 等待两个协程完成后再结束。
习惯用法:通道工厂模式 习惯用法:通道工厂模式
编程中常见另外一种模式如下不将通道作为参数传递给协程而用函数来生成一个通道并返回工厂角色函数内有个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`函数可以这样写,且在协程中调用这个动作,程序变成了这样:
示例 14.6-[channel_idiom2.go](examples/chapter_14/channel_idiom2.go) 它从指定通道中读取数据直到通道关闭,才继续执行下边的代码。很明显,另外一个协程必须写入 `ch`(不然代码就阻塞在 for 循环了),而且必须在写入完成后才关闭。`suck` 函数可以这样写,且在协程中调用这个动作,程序变成了这样:
示例 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,13 +561,15 @@ func processChannel(in <-chan int, out chan<- string) {
} }
} }
``` ```
通过使用方向注解来限制协程对通道的操作 通过使用方向注解来限制协程对通道的操作
这里有一个来自Go指导的很赞的例子打印了输出的素数使用选择器作为它的算法每个prime都有一个选择器如下图 这里有一个来自 Go 指导的很赞的例子打印了输出的素数使用选择器作为它的算法每个 prime 都有一个选择器如下图
![](../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)