2016-01-02 14:05:49 +00:00
### 8.4.1. 不帶緩存的Channels
2016-01-04 03:01:27 +00:00
一個基於無緩存Channels的發送操作將導致發送者goroutine阻塞, 直到另一個goroutine在相同的Channels上執行接收操作, 當發送的值通過Channels成功傳輸之後, 兩個goroutine可以繼續執行後面的語句。反之, 如果接收操作先發送, 那麽接收者goroutine也將阻塞, 直到有另一個goroutine在相同的Channels上執行發送操作。
基於無緩存Channels的發送和接收操作將導致兩個goroutine做一次同步操作。因爲這個原因, 無緩存Channels有時候也被稱爲同步Channels。當通過一個無緩存Channels發送數據時, 接收者收到數據發生在喚醒發送者goroutine之前( 譯註: *happens before*, 這是Go語言併發內存模型的一個關鍵術語! ) 。
在討論併發編程時, 當我們説x事件在y事件之前發生( *happens before*) , 我們併不是説x事件在時間上比y時間更早; 我們要表達的意思是要保證在此之前事件都已經完成了, 例如在此之前的更新某些變量的操作已經完成, 你可以放心依賴這些保證了。
當我們説x事件旣不是在y事件之前發生也不是在y事件之後發生, 我們就説x事件和y事件是併發的。這併不是意味着x事件和y事件就一定是同時發生的, 我們隻是不能確定這兩個事件發生的先後順序。在下一章中我們將看到, 當兩個goroutine併發訪問了相同的變量時, 我們有必要保證某些事件的執行順序, 以避免出現某些併發問題。
在8.3節的客戶端程序, 它在主goroutine中( 譯註: 就是執行main函數的goroutine) 將標準輸入複製到server, 因此當客戶端程序關閉標準輸入時, 後台goroutine可能依然在工作。我們需要讓主goroutine等待後台goroutine完成工作後再退出, 我們使用了一個channel來同步兩個goroutine:
```Go
gopl.io/ch8/netcat3
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done < - struct { } { } / / signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
< -done / / wait for background goroutine to finish
}
```
當用戶關閉了標準輸入, 主goroutine中的mustCopy函數調用將返迴, 然後調用conn.Close()關閉讀和寫方向的網絡連接。關閉網絡鏈接中的寫方向的鏈接將導致server程序收到一個文件( end-of-file) 結束的信號。關閉網絡鏈接中讀方向的鏈接將導致後台goroutine的io.Copy函數調用返迴一個“read from closed connection”( “從關閉的鏈接讀”) 類似的錯誤, 因此我們臨時移除了錯誤日誌語句; 在練習8.3將會提供一個更好的解決方案。( 需要註意的是go語句調用了一個函數字面量, 這Go語言中啟動goroutine常用的形式。)
在後台goroutine返迴之前, 它先打印一個日誌信息, 然後向done對應的channel發送一個值。主goroutine在退出前先等待從done對應的channel接收一個值。因此, 總是可以在程序退出前正確輸出“done”消息。
基於channels發送消息有兩個重要方面。首先每個消息都有一個值, 但是有時候通訊的事實和發生的時刻也同樣重要。當我們更希望強調通訊發生的時刻時, 我們將它稱爲**消息事件**。有些消息事件併不攜帶額外的信息, 它僅僅是用作兩個goroutine之間的同步, 這時候我們可以用`struct{}`空結構體作爲channels元素的類型, 雖然也可以使用bool或int類型實現同樣的功能, `done < - 1 ` 語句也比 ` done < - struct { } { } ` 更短 。
**練習 8.3: ** 在netcat3例子中, conn雖然是一個interface類型的值, 但是其底層眞實類型是`*net.TCPConn`, 代表一個TCP鏈接。一個TCP鏈接有讀和寫兩個部分, 可以使用CloseRead和CloseWrite方法分别關閉它們。脩改netcat3的主goroutine代碼, 隻關閉網絡鏈接中寫的部分, 這樣的話後台goroutine可以在標準輸入被關閉後繼續打印從reverb1服務器傳迴的數據。( 要在reverb2服務器也完成同樣的功能是比較睏難的; 參考**練習 8.4: **)