go 變數在其中一個函式中賦值 另一個函式_分享go的channel兩篇文章(1)
本文翻譯自Channels In Go
Channel是Go中一個重要的內建功能。這是讓Go獨一無二的功能之一,除了另一個獨特的功能,goroutine。這兩個獨特的功能使Go中的併發程式設計變得非常方便和有趣,這兩個特性降低了併發程式設計的難度。
本文將列出所有與channel相關的概念,語法和準則。為了更好地理解channel,還簡單描述了channel的內部結構和標準Go編譯器/執行時的一些實現細節。
對於新的gopher而言,本文的內容可能過於密集。某些部分可能需要閱讀多次才能消化。
併發程式設計和併發同步
現代CPU通常具有多個核心,而某些CPU核心支援超執行緒。換句話說,現代CPU可以同時處理多個指令流水線。為了充分利用現代CPU的強大功能,我們需要在編寫程式時進行併發程式設計。
併發計算是一種計算形式,其在重疊的時鐘週期內同時執行若干計算。下圖描繪了兩個併發計算案例。在圖中,A和B代表兩個單獨的計算。第二種情況也成為平行計算,它是特殊的併發計算。在第一種情況下,A和B僅在一小段時間內並行。
併發計算可以在程式,計算機和網路中發生。在這裡,我們只討論程式範圍內的併發計算。Goroutine是建立併發計算的Go方法。 併發計算可以共享資源,通常是記憶體資源。在併發計算中可能會發生某些情況:在同一時段中,一條指令會將資料寫入記憶體段中,而另一條指令會從相同的記憶體段中讀取資料。此時,資料讀取的完整性將無法得到保證。
在同一時段,兩條不同的指令會向相同的記憶體段中寫入資料。此時,儲存在記憶體段中的資料完整性將無法得到保證。
確定需要多少計算量
確定何時開始,阻塞,取消阻塞和結束計算
確定如何在併發的計算間分配工作負載
Channel 概述
對於併發程式設計的一個建議(由Rob Pike提出)不是(讓計算)通過共享記憶體進行通訊,而是讓它們通過通訊(channel)共享記憶體。
通過通訊共享記憶體和通過共享記憶體進行通訊是併發程式設計中的兩種程式設計方式。當goroutines通過共享記憶體進行通訊時,我們需要使用一些傳統的併發同步技術(如互斥鎖)來保護共享記憶體以防止資料爭用。我們可以使用channels來實現通過通訊共享記憶體。
Go提供了一種獨特的併發同步技術,channel。Channels使得goroutine通過通訊共享記憶體。我們可以將channel視為程式中的內部FIFO資料佇列。一些goroutine將值傳送到佇列(channel),而其他一些goroutine則從佇列中接收值。
除了傳遞值,一些值的所有權也可以在goroutine之間傳遞。當goroutine向channel傳送一個值時,我們可以看到goroutine釋放了某些值的所有權。當goroutine從channel接收到一個值時,我們可以看到goroutine獲取了某些值的所有權。實際的資料通常被轉移的值所引用。當然,也可能沒有任何所有權隨通訊channel一起轉移。
請注意,在這裡,當我們討論所有權時,我們指的是邏輯試圖中的所有權。與Rust語言不同,Go不確保語法級別的值所有權。Go channel可以幫助程式設計師輕鬆地編寫防止資料競爭的程式碼,但Go channel無法阻止程式設計師編寫錯誤的併發程式碼。
雖然Go還支援傳統的併發同步技術。只有channel才是Go的一級公民。Channel是Go中的一種型別,因此我們可以在不匯入任何包的情況下使用channel。另一方面,在
sync
和
sync/atomic
包中提供了那些傳統的併發同步技術。
老實說,每種併發同步技術都有自己的最佳使用場景。但channel擁有更廣泛的使用範圍。channel的一個問題是,使用channel程式設計的體驗是如此愉快和有趣,以至於程式設計師甚至更喜歡將channel用於一些不適合的場景。
Channel 的型別和值
像資料,切片和map一樣,每個channel都有一個元素型別。channel只能傳輸該型別的值。 channel可以是雙向或單向的。假設T是任意型別:chan T
表示雙向通道型別。編譯器允許從雙向channel中接收值和向雙向channel傳送值。chan
表示僅傳送通道型別。編譯器不允許從僅傳送channel接收值表示僅接收通道型別。編譯器不允許向僅接收channel傳送值
chan T
的值可以隱式地轉換為僅傳送型別
chan
和僅接收型別
,但反之則不然。僅傳送型別
chan
的值不能轉換為僅接收型別
,反之亦然。請注意,channel型別文字中的
符號是修飾符。
每個channel都有一個容量,將在下一節中介紹。具有零容量的channel稱為無緩衝channel,具有非零容量的channel稱為緩衝channel。
channel型別的零值用識別符號
nil
表示。必須使用內建的
make
函式建立非零channel值。例如,
make(chan int, 10)
將建立一個元素型別為
int
的channel。
make
函式的第二個引數指定新建立的channel的容量。第二個引數是可選的,其預設值為零。
Channel的賦值和比較
所有的channel型別都是可比較型別。 非零的channel值是多部分(multi-part values)值。在將一個channel值賦值給另一個channel值之後,這兩個channel共享相同的基礎部分。換句話說,兩個channel代表相同的內部channel物件。比較它們的結果是true
。
Channel 操作
有五個channel特定的操作。假設ch
是一個channel型別的變數,這裡列出了這些操作的語法和函式呼叫。
1.關閉通道
close(ch)
close
是一個內建函式。
close
函式的引數必須是channel型別的變數,而且
ch
不能是隻接收型別的通道。
2.傳送一個值
v
到通道中
ch
其中
v
必須是可分配給通道
ch
的元素型別的值,並且通道
ch
不能是僅接收型別的通道。請注意,此處
是一個channel傳送操作符。
3.從通道中接收一個值
channel接收操作始終返回至少一個結果,該結果是通道的元素型別的值,並且通道
ch
不能是僅傳送通道。請注意,此處
是channel接收操作符。是的,它的表示與channel傳送操作符相同。
對於大多數情況,channel接收操作被視為單值表示式。但是,當channel操作符用於賦值中唯一的源值表示式時,它可以生成第二個可選的布林值,併成為多值表示式。布林值表示在關閉channel之前是否傳送了相應的值。(下面我們將瞭解到我們可以從一個關閉的channel中接收到無限數量的值)
v =
v, sentBeforeClosed =
4.查詢channel的容量
cap(ch)
cap
是一個內建函式,它曾在Go的容器中引入。
cap
函式呼叫的返回結果是
int
值
5.查詢channel的快取中當前儲存了多少元素
len(ch)
len
是一個內建函式。
len
函式呼叫的返回值是
int
值。函式返回的結果表示已經成功傳送但尚未接收的元素數。
所有這些操作都是已同步的,因此無需進一步同步即可安全地執行這些操作。但是,與Go中的大多數其他操作一樣,channel的賦值不是同步操作,類似的,儘管任何channel的接收操作是同步的,但是將接收到的值分配給其他值也不是同步的。
如果查詢的channel是
nil
,那麼
cap
和
len
函式都返回0。這兩個查詢操作非常簡單,以後不再進一步解釋。實際上,這兩種操作在實踐中很少使用。
Channel 操作的細節說明
為了使channel操作的解釋簡單明瞭,在本文的其餘部分,channel將分為三類進行討論:1. nil channel.
2. non-nil but closed channel.
3. not-closed non-nil channel. 下表簡要歸納了各種channel的操作行為。
操作 | A Nil Channel | A Closed Channel | A Not-Closed Non-Nil Channel |
---|---|---|---|
Close | panic | panic | succeed to close(C) |
Send | block for ever | panic | block or succeed to send(B) |
Receive | block for ever | never block(D) | block or succeed to receive(A) |
關閉一個nil或已關閉的channel將導致panic
向一個已關閉的channel中傳送值也將導致panic
向一個nil channel中傳送值或從一個nil channel中接收值都將導致永久阻塞
接收goroutine佇列。該佇列是沒有大小限制的連結串列結構。此佇列中的goroutine都處於阻塞狀態並等待從該通道中接收值
傳送goroutine佇列。該佇列也是沒有大小限制的連結串列。此佇列中goroutine都處於阻塞狀態並等待向該通道中傳送值。每個goroutine嘗試傳送的值(或值的地址,取決於編譯器實現)也與該goroutine一起儲存在佇列中
值緩衝佇列。這是一個迴圈佇列。它的大小等於通道的容量。儲存在此緩衝區佇列中的值的型別是該通道的所有元素型別。如果儲存在通道的值緩衝佇列中的當前值的數量達到通道的容量,則通道狀態為滿狀態。如果當前通道的值緩衝區佇列中沒有儲存任何值,則通道狀態為空狀態。對於零容量(無緩衝)通道,它始終處於滿或空狀態。
Gr
試圖從未關閉的非零通道接收值時,goroutine
Gr
將首先獲取與通道關聯的鎖,然後執行以下步驟直到滿足一個條件。
- 如果通道的緩衝區佇列不為空,在這種情況下,通道的接收goroutine佇列必為空,goroutine
Gr
將從緩衝區佇列接收一個值。如果通道的傳送goroutine佇列也不為空,則一個傳送goroutine將會從傳送goroutine佇列中推出,並再次恢復為執行狀態。剛剛推出的傳送goroutine嘗試傳送的值將被推送到通道的值緩衝區佇列中。接收goroutineGr
繼續執行。對於此場景,通道接收操作稱為非阻塞操作。 - 否則(通道的值緩衝區佇列為空),如果通道的傳送goroutine佇列不為空,在這種情況下通道必然是無緩衝通道,接收goroutine
Gr
將從傳送goroutine佇列中推出一個傳送goroutine,並接收剛剛推出的傳送goroutine嘗試傳送的值。剛剛推出的傳送goroutine將會解除阻塞並恢復執行狀態。對於此場景,通道接收操作稱為非阻塞操作。 - 如果值緩衝佇列和通道的傳送goroutine佇列都為空,則goroutine
Gr
將被推入通道的接收goroutine佇列並進入(並保持)阻塞狀態。當另一個goroutine稍後向該通道傳送值時,它可以恢復到執行狀態。對於此場景,通道接收操作稱為阻塞操作。
Gs
嘗試向非關閉非零通道傳送值時,goroutine
Gs
將首先獲取與通道關聯的鎖,然後執行以下步驟直到滿足一個條件。
如果通道的接收goroutine佇列不為空,在這種情況下,通道的值緩衝佇列必然為空,傳送goroutine
Gs
將從通道的接收goroutine佇列中推出接收goroutine並將值傳送到剛剛推出的接收goroutine中。剛剛推出的goroutine將被解除阻塞並恢復到執行狀態。傳送goroutineGs
繼續執行。對於此場景,通道傳送操作稱為非阻塞操作。否則(接收goroutine佇列為空),如果通道的值緩衝區佇列未滿,在這種情況下,傳送goroutine佇列也必為空,傳送goroutine
Gs
嘗試傳送的值將被推入值緩衝區佇列,傳送goroutineGr
繼續執行。對於此場景,通道傳送操作稱為非阻塞操作。如果接收goroutine佇列為空並且通道的值緩衝區佇列已滿,則傳送goroutine
Gs
將被推入通道的傳送goroutine佇列並進入(並保持)阻塞狀態。當另一個goroutine稍後從通道接收值時,它可以恢復到執行狀態。對於此場景,通道傳送操作稱為阻塞操作。
如果通道的接收goroutine佇列不為空,在這種情況下,通道的緩衝區必為空,通道的接收goroutine佇列中的所有goroutine將逐一取消,每個goroutine將接收到通道元素型別到零值,並恢復到執行狀態。
如果通道的傳送goroutine佇列不為空,則通道的傳送goroutine佇列中的所有goroutine將被逐一取消,並且每個goroutine會因為向已關閉的channel中傳送值而發生panic。已經被推入通道的值緩衝區的值仍然存在。
false
,則第一個返回結果必然是通道元素型別的零值。
瞭解什麼是阻塞和非阻塞通道傳送或接收操作對於理解
select
控制流程塊的機制非常重要,這將在後面的部分介紹。
根據上面列出的解釋,我們可以得到一些關於通道內部佇列的事實。
如果通道關閉,則其傳送goroutine佇列和接收goroutine佇列都必須為空,但其值緩衝區佇列可能不為空
在任何時候,如果值緩衝區佇列不為空,則其接收goroutine佇列必為空
在任何時候,如果值緩衝區未滿,則其傳送goroutine佇列必為空
如果通道是緩衝的,那麼在任何時候,其傳送goroutine佇列和接收goroutine佇列之一必為空
如果通道是非緩衝的,那麼在任何時候,通常其傳送goroutine佇列和接收goroutine佇列中的一個必為空,但是在執行
select
控制流時會存在例外。
Channel 使用樣例
一個簡單的請求/響應示例。這個例子中的兩個goroutine通過一個無緩衝的通道互相交談。package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // an unbuffered channel
go func(ch chan int, x int) {
time.Sleep(time.Second)//
ch x*x // blocking here until the result is received}(c, 3)
done := make(chan struct{})go func(ch chan int) {
n := ch // blocking here until 9 is sent
fmt.Println(n) // 9// ch
time.Sleep(time.Second)
done struct{}{}}(c)done // blocking here until a value is sent to channel "done"
fmt.Println("bye")}
使用緩衝通道的演示。該程式不是併發的,它只是為了展示如何使用緩衝通道。
package main
import "fmt"
func main() {
c := make(chan int, 2) // a buffered channel
c 3
c 5close(c)
fmt.Println(len(c), cap(c)) // 2 2
x, ok := c
fmt.Println(x, ok) // 3 true
fmt.Println(len(c), cap(c)) // 1 2
x, ok = c
fmt.Println(x, ok) // 5 true
fmt.Println(len(c), cap(c)) // 0 2
x, ok = c
fmt.Println(x, ok) // 0 false
x, ok = c
fmt.Println(x, ok) // 0 false
fmt.Println(len(c), cap(c)) // 0 2close(c) // panic!
c 7 // also panic if the above close call is removed.}
一個永不結束的足球遊戲
packagemain
import ( "fmt"
"time"
)
func main() {
var ball = make(chan string)
kickBall := func(playerName string) {
for {
fmt.Println(ball, "kicked the ball.")
time.Sleep(time.Second)
ball playerName}}go kickBall("John")go kickBall("Alice")go kickBall("Bob")go kickBall("Emily")
ball "referee" // kick offvar c chan bool // nilc // blocking here for ever}
Channel 元素值按值傳遞
當值從一個goroutine傳遞到另一個goroutine時,該值將至少複製一次。如果傳輸的值保留在通道的值緩衝區中,則在傳輸過程中將產生兩個副本。當值從傳送方goroutine複製到值緩衝區時發生一個副本,另一個發生在將值從值緩衝區複製到接收方goroutine時。 對於標準Go編譯器,通道元素型別的大小必須小於65536。但是,通常,我們不應該建立具有大尺寸元素型別的通道,以避免在goroutine之間傳遞值的過程中過大的複製成本。因此,如果傳遞的值大小太大,最好使用指標型別,以避免大的複製成本。關於Channel和Goroutine的垃圾收集
注意,通道的傳送或接收goroutine佇列中的所有goroutine都引用了該通道,因此,如果通道的兩個佇列都不為空,則通道不會被垃圾回收。另一方面,如果goroutine被阻塞並且停留在通道的傳送或接收佇列中,則goroutine也將不會被垃圾收集,即使該通道僅由該goroutine引用。實際上,goroutine只能在已經退出後進行垃圾回收。Channel 傳送和接收操作都是簡單語句
通道傳送操作和接收操作都是簡單語句。通道接收操作可以始終用作單值表示式。簡單語句和表示式語句可用於基本流程控制塊的某些部分。 一個簡單示例,其中通道傳送和接收操作在控制流程塊中顯示為兩個簡單語句package mainimport ("fmt""time")func main() {
fibonacci := func() chan uint64 {
c := make(chan uint64)go func() {var x, y uint64 = 0, 1for ; y < (1 << 63); c y { // here
x, y = y, x+y}close(c)}()return c}
c := fibonacci()for x, ok := c; ok; x, ok = c { // here
time.Sleep(time.Second)
fmt.Println(x)}}
for-range
On Channel
for-range
控制流適用於通道。迴圈將嘗試迭代地接收發送到通道的值,直到通道關閉且其緩衝區佇列變為空。與陣列,切片和map上的
for-range
語法不同,用於儲存接收值的單個迭代變數允許存在於通道上的
for-range
語法中。
for v = range aChannel {// use v}
等同於
for {
v, ok = aChannelif !ok {break}// use v}
當然,此處
aChannel
的值不能是僅傳送通道。如果它是一個
nil
通道,那麼迴圈將永遠阻塞。
select-case
控制流
有一個特殊的
select-case
程式碼塊語法,專門為通道設計。語法很像
switch-case
語法。例如,在
select
程式碼塊中可以由多個
case
分支和至多一個
default
分支。但它們之間也存在一些明顯但差異。
不允許任何表示式或語句跟隨在
select
關鍵字之後(在{
之前)在
case
分支中不允許存在fallthrough
語句緊接在
case
關鍵字後的每個語句必須是通道接收操作或通道傳送操作。通道接收操作可以以簡單賦值語句的形式出現。如果存在一些非阻塞操作,Go執行時將隨機選擇其中一個分支執行
如果所有
case
分支中的操作都是阻塞操作,則如果default
分支存在,則將選擇default
分支執行。如果缺少default
分支,則當前goroutine將被推入每一個case
分支相關通道中的傳送goroutine佇列或接收goroutine佇列,然後進入阻塞狀態。
select-case
程式碼塊
select{}
將使當前goroutine永遠保持阻塞狀態。
下面的程式將進入
default
分支
package mainimport "fmt"func main() {var c chan struct{} // nilselect {case c: // blocking operationcase c struct{}{}: // blocking operationdefault:
fmt.Println("Go here.")}}
一個樣例,演示如何使用try-send和try-receive:
package mainimport "fmt"func main() {
c := make(chan string, 2)
trySend := func(v string) {select {case c v:default: // go here if c is full.}}
tryReceive := func() string {select {case v := c: return vdefault: return "-" // go here if c is empty.}}trySend("Hello!")trySend("Hi!")trySend("Bye!") // fail to send, but will not blocked.
fmt.Println(tryReceive()) // Hello!
fmt.Println(tryReceive()) // Hi!
fmt.Println(tryReceive()) // -}
以下這個樣例有50%的機會panic。在這個例子中,兩個
case
操作都是非阻塞的
package mainfunc main() {
c := make(chan struct{})close(c)select {case c struct{}{}: // panic if this case is selected.case c:}}
作者:絕望的祖父連結:https://www.jianshu.com/p/faa63ba6d955
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。