This commit is contained in:
glight2000
2015-12-29 18:26:33 +08:00
4 changed files with 405 additions and 0 deletions

View File

@@ -35,3 +35,6 @@ Golang 编程245386165
|2015-12-10|15.0 15.1 15.2
|2015-12-12|15.3
|2015-12-22|15.4
|2015-12-25|14.1
|2015-12-28|14.2
|2015-12-30|14.3 14.4

View File

@@ -456,8 +456,175 @@ func (c *container) Iter () <- chan items {
return ch
}
```
在协程里一个for循环迭代容器c中的元素对于树或图的算法这种简单的for循环可以替换为深度优先搜索
调用这个方法的代码可以这样迭代容器:
```go
for x := range container.Iter() { ... }
```
可以运行在自己的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在两个线程上)。就有了一个特殊的生产者-消费者模式。如果程序在协程给通道写完值之前结束协程不会被回收设计如此。这种行为看起来是错误的但是通道是一种线程安全的通信。在这种情况下协程尝试写入一个通道而这个通道永远不会被读取这可能是个bug而并非期望它被静默的回收。
习惯用法:生产者消费者模式
假设你有`Produce()`函数来产生`Consume`函数需要的值。它们都可以运行在独立的协程中,生产者在通道中放入给消费者读取的值。整个处理过程可以替换为无限循环:
```go
for {
Consume(Produce())
}
```
## 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)
go sink(c)
func source(ch chan<- int){
for { ch <- 1 }
}
func sink(ch <-chan int) {
for { <-ch }
}
```
习惯用法管道和选择器模式
更具体的例子还有协程处理它从通道接收的数据并发送给输出通道
```go
sendChan := make(chan int)
reciveChan := make(chan string)
go processChannel(sendChan, receiveChan)
func processChannel(in <-chan int, out chan<- string) {
for inValue := range in {
result := ... /// processing inValue
out <- result
}
}
```
通过使用方向注解来限制协程对通道的操作
这里有一个来自Go指导的很赞的例子打印了输出的素数使用选择器作为它的算法每个prime都有一个选择器如下图
![](../images/14.2_fig14.2.png?raw=true)
版本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
// license that can be found in the LICENSE file.package main
package main
import "fmt"
// Send the sequence 2, 3, 4, ... to channel 'ch'.
func generate(ch chan int) {
for i := 2; ; i++ {
ch <- i // Send 'i' to channel 'ch'.
}
}
// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func filter(in, out chan int, prime int) {
for {
i := <-in // Receive value of new variable 'i' from 'in'.
if i%prime != 0 {
out <- i // Send 'i' to channel 'out'.
}
}
}
// The prime sieve: Daisy-chain filter processes together.
func main() {
ch := make(chan int) // Create a new channel.
go generate(ch) // Start generate() as a goroutine.
for {
prime := <-ch
fmt.Print(prime, " ")
ch1 := make(chan int)
go filter(ch, ch1, prime)
ch = ch1
}
}
```
协程`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
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
487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619
631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769
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)`打印出来
版本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
// license that can be found in the LICENSE file.
package main
import (
"fmt"
)
// Send the sequence 2, 3, 4, ... to returned channel
func generate() chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
ch <- i
}
}()
return ch
}
// Filter out input values divisible by 'prime', send rest to returned channel
func filter(in chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
out <- i
}
}
}()
return out
}
func sieve() chan int {
out := make(chan int)
go func() {
ch := generate()
for {
prime := <-ch
ch = filter(ch, prime)
out <- prime
}
}()
return out
}
func main() {
primes := sieve()
for {
fmt.Println(<-primes)
}
}
```
## 链接

110
eBook/14.3.md Normal file
View File

@@ -0,0 +1,110 @@
# 14.3 协程的同步:关闭通道-测试阻塞的通道
通道可以被显示的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。
继续看示例[goroutine2.go](examples/chapter_14/goroutine2.go)示例14.2):我们如何在通道的`sendData()`完成的时候发送一个信号,`getData()`又如何检测到通道是否关闭或阻塞?
第一个可以通过函数`close(ch)`来完成:这个将通道标记为无法通过发送操作<-接受更多的值给已经关闭的通道发送或者再次关闭都会导致运行时的panic在创建一个通道后使用defer语句是个不错的办法类似这种情况
```go
ch := make(chan float64)
defer close(ch)
```
第二个问题可以使用逗号ok操作符用来检测通道是否被关闭
如何来检测可以收到没有被阻塞或者通道没有被关闭
```go
v, ok := <-ch // ok is true if v received value
```
通常和if语句一起使用
```go
if v, ok := <-ch; ok {
process(v)
}
```
或者在for循环中接收的时候当关闭或者阻塞的时候使用break
```go
v, ok := <-ch
if !ok {
break
}
process(v)
```
可以通过`_ = ch <- v`来实现非阻塞发送因为空标识符获取到了发送给`ch`的任何东西在示例程序14.2中使用这些可以改进为版本goroutine3.go输出相同
实现非阻塞通道的读取需要使用select参见章节[14.4](14.4.md)
示例 14.9-[goroutine3.go](examples/chapter_14/goroutine3.go)
```go
package main
import "fmt"
func main() {
ch := make(chan string)
go sendData(ch)
getData(ch)
}
func sendData(ch chan string) {
ch <- "Washington"
ch <- "Tripoli"
ch <- "London"
ch <- "Beijing"
ch <- "Tokio"
close(ch)
}
func getData(ch chan string) {
for {
input, open := <-ch
if !open {
break
}
fmt.Printf("%s ", input)
}
}
```
改变了以下代码
* 现在只有`sendData()`是协程`getData()``main()`在同一个线程中
```go
go sendData(ch)
getData(ch)
```
* `sendData()`函数的最后关闭了通道
```go
func sendData(ch chan string) {
ch <- "Washington"
ch <- "Tripoli"
ch <- "London"
ch <- "Beijing"
ch <- "Tokio"
close(ch)
}
```
* 在for循环的`getData()`在每次接收通道的数据之前都使用`if !open`来检测
```go
for {
input, open := <-ch
if !open {
break
}
fmt.Printf("%s ", input)
}
```
使用for-range语句来读取通道是更好的办法因为这会自动检测通道是否关闭
```go
for input := range ch {
process(input)
}
```
阻塞和生产者-消费者模式
在章节14.2.10的通道迭代器中两个协程经常是一个阻塞另外一个如果程序工作在多核心的机器上大部分时间只用到了一个处理器可以通过使用带缓冲缓冲空间大于0的通道来改善比如缓冲大小为100迭代器在阻塞之前至少可以从容器获得100个元素如果消费者协程在独立的内核运行就有可能让协程不会出现阻塞
由于容器中元素的数量通常是已知的需要让通道有足够的容量放置所有的元素这样迭代器就不会阻塞尽管消费者协程仍然可能阻塞)。然后这样有效的加倍了迭代容器所需要的内存使用量所以通道的容量需要限制一下最大值记录运行时间和性能测试可以帮助你找到最小的缓存容量带来最好的性能
## 链接
- [目录](directory.md)
- 上一节[协程间的信道](14.2.md)
- 下一节[使用select切换协程](14.4.md)

125
eBook/14.4.md Normal file
View File

@@ -0,0 +1,125 @@
# 14.4 使用select切换协程
从不不同的并发执行的协程中获取值可以通过关键字`select`来完成,它和`switch`控制语句非常相似章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select`监听进入通道的数据,也可以是用通道发送值的时候。
```go
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
```
`default`语句是可选的fallthrough行为和普通的switch相似是不允许的。在任何一个case中执行`break`或者`return`select就结束了。
`select`做得就是:选择处理列出的多个通信情况中的一个。
* 如果都阻塞了,会等待直到其中一个可以处理
* 如果多个可以处理,随机选择一个
* 如果没有通道操作可以处理并且写了`default`语句,它就会执行:`default`永远是可运行的(这就是准备好了,可以执行)。
`select`中使用发送操作并且有`default`可以确保发送不被阻塞如果没有casesselect就会一直阻塞。
`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秒后结束。
示例 14.10-[goroutine_select.go](examples/chapter_14/goroutine_select.go)
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go pump1(ch1)
go pump2(ch2)
go suck(ch1, ch2)
time.Sleep(1e9)
}
func pump1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func pump2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func suck(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}
```
输出:
```
Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 0
Received on channel 2: 7
Received on channel 2: 8
Received on channel 2: 9
Received on channel 2: 10
Received on channel 1: 2
Received on channel 2: 11
...
Received on channel 2: 47404
Received on channel 1: 94346
Received on channel 1: 94348
```
一秒内的输出非常惊人如果我们给它计数goroutine_select2.go得到了90000个左右的数字。
##练习:
练习14.7
* a在练习5.4的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)
练习14.8
从示例6.10的斐波那契程序开始,制定解决方案,使斐波那契周期计算独立到协程中,并可以把结果发送给通道。
结束的时候关闭通道。`main()`函数读取通道并打印结果:[goFibonacci.go](exercises/chapter_14/gofibonacci.go)
使用练习6.9中的算法写一个更短的[gofibonacci2.go](exercises/chapter_14/gofibonacci2.go)
使用`select`语句来写,并让通道退出([gofibonacci_select.go](exercises/chapter_14/gofibonacci_select.go)
注意当给结果计时并和6.10对比时,我们发现使用通道通信的性能开销有轻微削减;这个例子中的算法使用协程并非性能最好的选择;但是[gofibonacci3](exercises/chapter_14/gofibonacci3.go)方案使用了2个协程带来了3倍的提速。
练习14.9
做一个随机位生成器程序可以提供无限的随机0或者1的序列[random_bitgen.go](exercises/chapter_14/random_bitgen.go)
练习14.10[polar_to_cartesian.go](exercises/chapter_14/polar_to_cartesian.go)
这是一种综合练习使用到章节4,9,11的内容和本章内容。
## 链接
- [目录](directory.md)
- 上一节:[通道的同步:关闭通道-测试阻塞的通道](14.3.md)
- 下一节:[通道,超时和计时器](14.5.md)