mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-11 20:11:26 +08:00
14.2
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
## 翻译进度
|
||||
|
||||
14.1 [并发、并行和协程](eBook/14.1.md)
|
||||
14.2 [使用通道进行协程间通信](eBook/14.2.md)
|
||||
|
||||
## 支持本书
|
||||
|
||||
|
@@ -31,4 +31,4 @@ Golang 编程:245386165
|
||||
|
||||
|更新日期 |更新内容
|
||||
|----------|------------------
|
||||
|2015-12-31|14.1 并发、并行和协程
|
||||
|2015-1-4|14.2 使用通道进行协程间通信
|
||||
|
203
eBook/14.2.md
203
eBook/14.2.md
@@ -16,18 +16,20 @@
|
||||
|
||||
未初始化的通道的值是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
|
||||
var ch1 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
|
||||
if <- ch != 1000{
|
||||
...
|
||||
...
|
||||
}
|
||||
```
|
||||
操作符 <- 也被用来发送和接收,Go尽管不必要,为了可读性,通道的命名通常以`ch`开头或者包含`chan`。通道的发送和接收操作都是自动的:它们通常一气呵成。下面的示例展示了通信操作。
|
||||
|
||||
操作符 <- 也被用来发送和接收,Go 尽管不必要,为了可读性,通道的命名通常以 `ch` 开头或者包含 `chan`。通道的发送和接收操作都是自动的:它们通常一气呵成。下面的示例展示了通信操作。
|
||||
|
||||
示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -86,42 +91,49 @@ func getData(ch chan string) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
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变空也结束了。
|
||||
* 如果我们移除一个或所有go关键字,程序无法运行,Go运行时会抛出panic:
|
||||
|
||||
- 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!
|
||||
```
|
||||
|
||||
为什么会这样?运行时会检查所有的协程(也许只有一个是这种情况)是否在等待(可以读取或者写入某个通道),意味着程序无法处理。这是死锁(deadlock)形式,运行时可以检测到这种情况。
|
||||
|
||||
注意:不要使用打印状态来表明通道的发送和接收顺序:由于打印状态和通道实际发生读写的时间延迟会导致和真实发生的顺序不同。
|
||||
|
||||
练习 14.4:解释一下为什么如果在函数`getData()`的一开始插入`time.Sleep(1e9)`,不会出现错误但也没有输出呢。
|
||||
练习 14.4:解释一下为什么如果在函数 `getData()` 的一开始插入 `time.Sleep(1e9)`,不会出现错误但也没有输出呢。
|
||||
|
||||
## 14.2.3 通道阻塞
|
||||
|
||||
默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的:
|
||||
|
||||
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待ch再次变为可用状态:就是通道值被接收时(可以传入变量)。
|
||||
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。
|
||||
|
||||
2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。
|
||||
|
||||
尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。
|
||||
|
||||
程序`channel_block.go`验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字0。
|
||||
程序 `channel_block.go` 验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字 0。
|
||||
|
||||
示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -139,13 +151,17 @@ func pump(ch chan int) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
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
|
||||
func suck(ch chan int) {
|
||||
for {
|
||||
@@ -153,27 +169,31 @@ func suck(ch chan int) {
|
||||
}
|
||||
}
|
||||
```
|
||||
在`main()`中使用协程开始它:
|
||||
|
||||
在 `main()` 中使用协程开始它:
|
||||
|
||||
```go
|
||||
go pump(ch1)
|
||||
go suck(ch1)
|
||||
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 通过一个(或多个)通道交换数据进行协程同步。
|
||||
|
||||
通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。
|
||||
|
||||
甚至可以在通道两端互相阻塞对方,形成了叫做死锁的状态。Go运行时会检查并panic,停止程序。死锁几乎完全是由糟糕的设计导致的。
|
||||
甚至可以在通道两端互相阻塞对方,形成了叫做死锁的状态。Go 运行时会检查并 panic,停止程序。死锁几乎完全是由糟糕的设计导致的。
|
||||
|
||||
无缓冲通道会被阻塞。设计无阻塞的程序可以避免这种情况,或者使用带缓冲的通道。
|
||||
|
||||
练习 14.2: [blocking.go](exercises/chapter_14/blocking.go)
|
||||
|
||||
解释为什么下边这个程序会导致panic:所有的协程都休眠了 - 死锁!
|
||||
解释为什么下边这个程序会导致 panic:所有的协程都休眠了 - 死锁!
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -194,49 +214,53 @@ func main() {
|
||||
|
||||
## 14.2.5 同步通道-使用带缓冲的通道
|
||||
|
||||
一个无缓冲通道只能包含1个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的`make`命令中设置它的容量,如下:
|
||||
一个无缓冲通道只能包含 1 个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的 `make` 命令中设置它的容量,如下:
|
||||
|
||||
```go
|
||||
buf := 100
|
||||
ch1 := make(chan string, buf)
|
||||
buf := 100
|
||||
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 -> asynchronous, buffered(非阻塞)取决于value元素
|
||||
- value == 0 -> synchronous, unbuffered (阻塞)
|
||||
- value > 0 -> asynchronous, buffered(非阻塞)取决于value元素
|
||||
|
||||
若使用通道的缓冲,你的程序会在“请求”激增的时候表现更好:更具弹性,专业术语叫:更具有伸缩性(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 协程中用通道输出结果
|
||||
|
||||
为了知道计算何时完成,可以通过信道回报。在例子`go sum(bigArray)`中,要这样写:
|
||||
为了知道计算何时完成,可以通过信道回报。在例子 `go sum(bigArray)` 中,要这样写:
|
||||
|
||||
```go
|
||||
ch := make(chan int)
|
||||
go sum(bigArray, ch) // bigArray puts the calculated sum on ch
|
||||
// .. do something else for a while
|
||||
sum := <- ch // wait for, and retrieve the sum
|
||||
```
|
||||
|
||||
也可以使用通道来达到同步的目的,这个很有效的用法在传统计算机中成为(semaphore)。或者换个方式:通过通道发送信号告知处理已经完成(在协程中)。
|
||||
|
||||
在其他协程运行时让main程序无限阻塞的通常做法是在`main`函数的最后放置一个{}。
|
||||
在其他协程运行时让 main 程序无限阻塞的通常做法是在 `main` 函数的最后放置一个{}。
|
||||
|
||||
也可以使用通道让`main`程序等待协程完成,就是所谓的信号量模式,我们会在接下来的部分讨论。
|
||||
也可以使用通道让 `main` 程序等待协程完成,就是所谓的信号量模式,我们会在接下来的部分讨论。
|
||||
|
||||
## 14.2.7 信号量模式
|
||||
|
||||
下边的片段阐明:协程通过在通道`ch`中放置一个值来处理结束的信号。`main`协程等待`<-ch`直到从中获取到值。
|
||||
下边的片段阐明:协程通过在通道 `ch` 中放置一个值来处理结束的信号。`main` 协程等待 `<-ch` 直到从中获取到值。
|
||||
|
||||
我们期望从这个通道中获取返回的结果,像这样:
|
||||
|
||||
```go
|
||||
func compute(ch chan int){
|
||||
ch <- someComputation() // when it completes, signal on the channel.
|
||||
@@ -249,8 +273,9 @@ func main(){
|
||||
result := <- ch
|
||||
}
|
||||
```
|
||||
这个信号也可以是其他的,不反回结果,比如下边这个协程中的lambda函数
|
||||
协程:
|
||||
|
||||
这个信号也可以是其他的,不反回结果,比如下边这个协程中的 lambda 函数协程:
|
||||
|
||||
```go
|
||||
ch := make(chan int)
|
||||
go func(){
|
||||
@@ -260,7 +285,9 @@ go func(){
|
||||
doSomethingElseForAWhile()
|
||||
<- ch // Wait for goroutine to finish; discard sent value.
|
||||
```
|
||||
|
||||
或者等待两个协程完成,每一个都会对切片s的一部分进行排序,片段如下:
|
||||
|
||||
```go
|
||||
done := make(chan bool)
|
||||
// doSort is a lambda function, so a closure which knows the channel done:
|
||||
@@ -274,7 +301,9 @@ go doSort(s[i:])
|
||||
<-done
|
||||
<-done
|
||||
```
|
||||
下边的代码,用完整的信号量模式对size长度的gloat64切片进行了N个`doSomething()`计算并同时完成,通道sem分配了相同的长度(切包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道sem不停的接收数据来等待所有的协程完成。
|
||||
|
||||
下边的代码,用完整的信号量模式对 size 长度的 gloat64 切片进行了 N 个` doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(切包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。
|
||||
|
||||
```go
|
||||
type Empty interface {}
|
||||
var empty Empty
|
||||
@@ -292,11 +321,13 @@ for i, xi := range data {
|
||||
// wait for goroutines to finish
|
||||
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
|
||||
for i, v := range data {
|
||||
go func (i int, v float64) {
|
||||
@@ -305,24 +336,28 @@ for i, v := range data {
|
||||
} (i, v)
|
||||
}
|
||||
```
|
||||
在for循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如Fortress或者其他并行框架以不同的结构实现了这种方式,在Go中用协程实现起来非常容易:
|
||||
|
||||
在 for 循环中并行计算迭代可能带来很好的性能提升。不过所有的迭代都必须是独立完成的。有些语言比如 Fortress 或者其他并行框架以不同的结构实现了这种方式,在 Go 中用协程实现起来非常容易:
|
||||
|
||||
## 14.2.9 用带缓冲通道实现一个信号量
|
||||
|
||||
信号量是实现互斥锁(排外锁)常见的同步机制,限制对资源的访问,解决读写问题,比如没有实现信号量的`sync`的Go包,使用带缓冲的通道可以轻松实现:
|
||||
信号量是实现互斥锁(排外锁)常见的同步机制,限制对资源的访问,解决读写问题,比如没有实现信号量的 `sync` 的 Go 包,使用带缓冲的通道可以轻松实现:
|
||||
|
||||
* 带缓冲通道的容量和我们要同步的资源容量相同
|
||||
* 通道的长度(当前存放的元素个数)当前资源被使用的数量相同
|
||||
* 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
|
||||
- 带缓冲通道的容量和我们要同步的资源容量相同
|
||||
- 通道的长度(当前存放的元素个数)当前资源被使用的数量相同
|
||||
- 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
|
||||
|
||||
不用管通道中存放的是什么,只关注长度;因此我们创建了一个有长度变量为 0(字节)的通道:
|
||||
|
||||
不用管通道中存放的是什么,只关注长度;因此我们创建了一个有长度变量为0(字节)的通道:
|
||||
```go
|
||||
type Empty interface {}
|
||||
type semaphore chan Empty
|
||||
```
|
||||
将可用资源的数量N来初始化信号量`semaphore`: `sem = make(semaphore, N)`
|
||||
|
||||
将可用资源的数量N来初始化信号量 `semaphore`:`sem = make(semaphore, N)`
|
||||
|
||||
然后直接对信号量进行操作:
|
||||
|
||||
```go
|
||||
// acquire n resources
|
||||
func (s semaphore) P(n int) {
|
||||
@@ -339,7 +374,9 @@ func (s semaphore) V(n int) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以用来实现一个互斥的例子:
|
||||
|
||||
```go
|
||||
/* mutexes */
|
||||
func (s semaphore) Lock() {
|
||||
@@ -359,15 +396,17 @@ func (s semaphore) Signal() {
|
||||
s.V(1)
|
||||
}
|
||||
```
|
||||
|
||||
练习 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()` 等待两个协程完成后再结束。
|
||||
|
||||
习惯用法:通道工厂模式
|
||||
|
||||
编程中常见另外一种模式如下:不将通道作为参数传递给协程,而用函数来生成一个通道并返回(工厂角色);函数内有个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
|
||||
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
|
||||
for v := range ch {
|
||||
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
|
||||
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
|
||||
func (c *container) Iter () <- chan items {
|
||||
ch := make(chan item)
|
||||
@@ -456,17 +499,21 @@ func (c *container) Iter () <- chan items {
|
||||
return ch
|
||||
}
|
||||
```
|
||||
在协程里,一个for循环迭代容器c中的元素(对于树或图的算法,这种简单的for循环可以替换为深度优先搜索)。
|
||||
|
||||
在协程里,一个 for 循环迭代容器 c 中的元素(对于树或图的算法,这种简单的 for 循环可以替换为深度优先搜索)。
|
||||
|
||||
调用这个方法的代码可以这样迭代容器:
|
||||
|
||||
```go
|
||||
for x := range container.Iter() { ... }
|
||||
```
|
||||
可以运行在自己的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在两个线程上)。就有了一个特殊的生产者-消费者模式。如果程序在协程给通道写完值之前结束,协程不会被回收;设计如此。这种行为看起来是错误的,但是通道是一种线程安全的通信。在这种情况下,协程尝试写入一个通道,而这个通道永远不会被读取,这可能是个bug而并非期望它被静默的回收。
|
||||
|
||||
可以运行在自己的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在两个线程上)。就有了一个特殊的生产者-消费者模式。如果程序在协程给通道写完值之前结束,协程不会被回收;设计如此。这种行为看起来是错误的,但是通道是一种线程安全的通信。在这种情况下,协程尝试写入一个通道,而这个通道永远不会被读取,这可能是个 bug 而并非期望它被静默的回收。
|
||||
|
||||
习惯用法:生产者消费者模式
|
||||
|
||||
假设你有`Produce()`函数来产生`Consume`函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环:
|
||||
假设你有 `Produce()` 函数来产生 `Consume` 函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环:
|
||||
|
||||
```go
|
||||
for {
|
||||
Consume(Produce())
|
||||
@@ -476,11 +523,14 @@ for {
|
||||
## 14.2.11 通道的方向
|
||||
|
||||
通道类型可以用注解来表示它只发送或者只接收:
|
||||
|
||||
```go
|
||||
var send_only chan<- int // channel can only receive data
|
||||
var recv_only <-chan int // channel can onley send data
|
||||
```
|
||||
|
||||
只接收的通道(<-chan T)无法关闭,因为关闭通道是发送者用来表示不再给通道发送值了,所以对只接收通道是没有意义的。通道创建的时候都是双向的,但也可以分配有方向的通道变量,就像以下代码:
|
||||
|
||||
```go
|
||||
var c = make(chan int) // bidirectional
|
||||
go source(c)
|
||||
@@ -498,6 +548,7 @@ func sink(ch <-chan int) {
|
||||
习惯用法:管道和选择器模式
|
||||
|
||||
更具体的例子还有协程处理它从通道接收的数据并发送给输出通道:
|
||||
|
||||
```go
|
||||
sendChan := make(chan int)
|
||||
reciveChan := make(chan string)
|
||||
@@ -510,13 +561,15 @@ func processChannel(in <-chan int, out chan<- string) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过使用方向注解来限制协程对通道的操作。
|
||||
|
||||
这里有一个来自Go指导的很赞的例子,打印了输出的素数,使用选择器(‘筛’)作为它的算法。每个prime都有一个选择器,如下图:
|
||||
这里有一个来自 Go 指导的很赞的例子,打印了输出的素数,使用选择器(‘筛’)作为它的算法。每个 prime 都有一个选择器,如下图:
|
||||
|
||||

|
||||
|
||||
版本1: 示例 14.7-[sieve1.go](examples/chapter_14/sieve1.go)
|
||||
版本1:示例 14.7-[sieve1.go](examples/chapter_14/sieve1.go)
|
||||
|
||||
```go
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// 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
|
||||
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
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
```go
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
@@ -581,7 +640,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Send the sequence 2, 3, 4, ... to returned channel
|
||||
// Send the sequence 2, 3, 4, ... to returned channel
|
||||
func generate() chan int {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
@@ -629,5 +688,5 @@ func main() {
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[并发,并行和协程](14.1.md)
|
||||
- 上一节:[并发、并行和协程](14.1.md)
|
||||
- 下一节:[协程同步:关闭通道-测试阻塞的通道](14.3.md)
|
||||
|
Reference in New Issue
Block a user