diff --git a/eBook/14.2.md b/eBook/14.2.md index f4e508e..44ca38f 100644 --- a/eBook/14.2.md +++ b/eBook/14.2.md @@ -49,8 +49,161 @@ if <- ch != 1000{ ... } ``` +操作符 <- 也被用来发送和接收,Go尽管不必要,为了可读性,通道的命名通常以`ch`开头或者包含`chan`。通道的发送和接收操作都是自动的:它们通常一气呵成。下面的示例展示了通信操作。 +示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go) +```go +package main +import ( + "fmt" + "time" +) + +func main() { + ch := make(chan string) + + go sendData(ch) + go getData(ch) + + time.Sleep(1e9) +} + +func sendData(ch chan string) { + ch <- "Washington" + ch <- "Tripoli" + ch <- "London" + ch <- "Beijing" + ch <- "Tokio" +} + +func getData(ch chan string) { + var input string + // time.Sleep(1e9) + for { + input = <-ch + fmt.Printf("%s ", input) + } +} +``` +输出: +``` +Washington Tripoli London Beijing Tokio +``` +`main()`函数中启动了两个协程:`sendData()`通过通道ch发送了5个字符串,`getData()`按顺序接收它们并打印出来。 + +如果2个协程需要通信,你必须给他们同一个通道作为参数才行。 + +尝试一下如果注释掉`time.Sleep(1e9)`会如何。 + +我们发现协程之间的同步非常重要: +* 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.2.3 通道阻塞 + +默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的: + +1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待ch再次变为可用状态:就是通道值被接收时(可以传入变量)。 + +2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。 + +尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。 + +程序`channel_block.go`验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字0。 + +示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go) +```go +package main + +import "fmt" + +func main() { + ch1 := make(chan int) + go pump(ch1) // pump hangs + fmt.Println(<-ch1) // prints only 0 +} + +func pump(ch chan int) { + for i := 0; ; i++ { + ch <- i + } +} +``` +输出: +``` +0 +``` +`pump()`函数为通道提供数值,也被叫做生产者。 + +为通道解除阻塞定义了`suck`函数来在无限循环中读取通道,参见示例 14.4-[channel_block2.go](examples/chapter_14/channel_block2.go): +```go +func suck(ch chan int) { + for { + fmt.Println(<-ch) + } +} +``` +在`main()`中使用协程开始它: +```go +go pump(ch1) +go suck(ch1) +time.Sleep(1e9) +``` +给程序1秒的时间来运行:输出了上万个整数。 + +练习 14.1:[channel_block3.go](exercises/chapter_14/channel_block3.go):写一个通道证明它的阻塞性,开启一个协程接收通道的数据,持续15秒,然后给通道放入一个值。在不同的阶段打印消息并观察输出。 + +## 14.2.4 通过一个(或多个)通道交换数据进行协程同步。 + +通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。 + +甚至可以在通道两端互相阻塞对方,形成了叫做死锁的状态。Go运行时会检查并panic,停止程序。死锁几乎完全是由糟糕的设计导致的。 + +无缓冲通道会被阻塞。设计无阻塞的程序可以避免这种情况,或者使用带缓冲的通道。 + +练习 14.2: [blocking.go](exercises/chapter_14/blocking.go) + +解释为什么下边这个程序会导致panic:所有的协程都休眠了 - 死锁! +```go +package main + +import ( + "fmt" +) + +func f1(in chan int) { + fmt.Println(<-in) +} + +func main() { + out := make(chan int) + out <- 2 + go f1(out) +} +``` + +## 14.2.5 同步通道-使用带缓冲的通道 + +一个无缓冲通道只能包含1个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的`make`命令中设置它的容量,如下: +```go + buf := 100 + ch1 := make(chan string, buf) +``` +buf是通道可以承受的元素(这里是string)个数 + +在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。 + +缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。