go基礎8-Goroutines和Channels
Goroutines
程序,執行緒和協程區別:
程序和執行緒:核心進行排程,有cpu時間片的概念,進行搶佔式排程;相互間更公平,但是資源佔用高;
協程:使用者級執行緒,對核心透明,程式自行排程,通常只能進行協作式排程,需要協程主動讓出控制權;相互間執行不公平,無法直接利用多核優勢,但資源佔用低;
goroutine(協程):是go的併發執行單元.簡單上可以理解為其他語言的執行緒.協程相對獨立,有自己的上下文,切換由自己控制(執行緒是由系統控制);當程式啟動時,主函式執行在一個單獨的goroutine中,我們叫它main goroutine;新的goroutine會用go語句來建立;
go中所有系統呼叫操作都會出讓cpu給其他goroutine,這使得goroutine切換不依賴系統執行緒和程序,也不依賴cpu的核數;
協程原理
多執行緒程式設計:執行緒多則上下文切換頻繁,cpu時間消耗大;
非同步程式設計:針對上面一個執行緒一個socket連線的消耗問題,通過少了執行緒來服務大量的網路連線和I/O操作;這樣會讓程式碼複雜,易出錯(執行無序);
協程:應用模擬執行緒,避免上下文切換,降低併發複雜度;兼顧了併發和效能;
原理:和執行緒一樣,維護一個執行緒棧,當a執行緒切換到b執行緒是,需要將a執行緒的相關執行進度壓入棧,然後將b執行緒的執行進度出棧,進入b執行緒的執行序列;只不過協程是在應用層實現這一點;
問題:
1 應用程式沒有cpu呼叫許可權,無法直接操作執行緒入棧,出棧執行問題?
解決:協程是基於執行緒的,內部維護一組資料結構和n個執行緒,真正執行還是執行緒,協程執行程式碼被扔進一個待執行佇列,由n個執行緒從佇列拉取執行;
2 協程如何切換,即非同步執行問題?
解決:利用作業系統的非同步函式實現,包括linux的epoll,select,windows的iocp,event等.go通過封裝各種io函式,這些io函式呼叫了作業系統底層的非同步io函式,當非同步函式返回busy或blocking時,go就進行現有執行序列壓棧,讓執行緒拉取另一個協程程式碼執行.
語法:go關鍵字+函式或方法;go語句會使用其語句中的函式在新建立的goroutine中執行.
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
主函式返回時,所有的goroutine都會被直接打斷,程式退出.
func main() { go spinner(100 * time.Millisecond) // 在新建立的goroutine中執行spinner函式 } func spinner(delay time.Duration) { fmt.Printf(delay) }
goroutine退出方式:
- 主函式退出
- 直接終止程式
- goroutine請求觸發其他goroutine自動結束執行
goroutine使用需要考慮呼叫時是否安全.goroutine是協程,它比執行緒更小,十幾個goroutine可能底層只有五六個執行緒,記憶體消耗也更小,單個goroutine大概佔用4-5kb棧記憶體.它比執行緒更易用,更高效,更輕便;
runtime包
Gosched 禮讓協程
Goexit 協程結束執行
GOMAXPROCS 設定平行計算cpu核數最大值
Channels
Channels:協程通訊通道.它是一個通訊機制,用於goroutine間傳送訊息.它可以傳送資料的型別
建立channel
ch := make(chan int) // 通道型別是int型別
channel是個值引用型別,零值為nil;也因此可以進行==比較;
channal的通訊行為包括:傳送和接收;傳送和接收的操作都使用<-運算子;傳送時<-位於channel和值之間;接收<-位於channel前;
ch <- x // 傳送
x = <-ch // 接收
<-ch // 接收,忽略結果
通道關閉後,傳送會產生panic;接收可以正常接收通道里的值,通道為空時返回零值資料;
close(ch) // 用於關閉通道
建立通道時,容量大於零則表示為帶快取channel
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
不帶快取的channel
無快取channel也稱為同步channel:它會導致兩個goroutine進行同步操作,goroutine傳送資料後會阻塞傳送至訊息被接收;反之一樣,接收先發生,接收者goroutine將阻塞等待訊息傳送;
happens before:接收者收到資料發生在再次喚醒傳送者goroutine之前;x事件在y事件之前發生,並不是強調x發生時間比y早,而是要強調保證y前x會完成;
併發:x事件不發生在y事件之前或之後;這裡只是無法區分x,y發生的事件先後,而不是一定同時發生;
訊息事件:強調通訊發生的時刻(事實),而不是訊息具體值的情形;很多時候訊息事件不沒有具體值;
pipeline(串聯的channels)
pipeline:是將多個channel連線在一起.由於是同步channel,channel越多越可能因為阻塞造成死迴圈;如果channel資料傳送完畢可以通過close關閉,避免阻塞發生;但是關閉會造成傳送panic,接收無限獲取nil值;實際不需要關閉每一個channel,當channel沒有被引用時,go會進行垃圾自動回收;
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
實際只需要在業務執行完再進行關閉即可
單方向的channel
單方向的channel:語法上限制channel只能用於傳送或接收.通過這種限制方式,來避免使用混亂問題,也可以避免位置的panic;
傳送: chan <- int 表示只發送int的channel
接收: <- chan int 表示只能接收int的channel
因為關閉操作只用於斷言不再向channel傳送新資料,所以只有傳送方的goroutine才會呼叫close函式;
可以將channel轉換為單方向channel,但是不能將單方向channel轉成正常的channel,這種轉換是單向的;
帶快取的channels
帶快取的Channel內部持有一個元素佇列.
ch = make(chan string, 3)
向快取channel傳送操作就是向內部快取佇列的尾部插入元素,接收操作時從佇列的頭部刪除元素;如果佇列是滿的,則阻塞傳送;如果佇列是空的,則阻塞接收;
查詢channel容量
cap(ch)
查詢數量
len(ch)
channel和goroutine的排程器機制緊密相連,如果沒有其他goroutine從channel傳送或接收,將會有永遠阻塞的風險(deadlock)
注意事項:
- 無快取的channel,存在無接收方引起的goroutines洩漏問題;因為傳送一直被佔用;
- 無快取channel用於保證每個傳送操作與相應的同步接收操作;帶快取channel用於解耦通訊操作;
- 帶快取channel的容量規劃也很關鍵,因為容量大的話,會造成傳送快取(接收閒置,資源利用不佳);容量小時,又會造成程式死鎖(傳送堆積);
- 帶快取channel可能影響程式效能;傳送和接收的效率不一致,會造成程式閒置,效能不佳;
goroutine與channel
併發中,當goroutine執行時由於非同步執行的原因,並不會等待執行結果,它會在觸發非同步後直接返回,可能會造成執行中斷或無法獲取結果的問題;這時就需要channel來進行goroutine間通訊;
- select多路複用
它和switch語句有點像,它用於選擇通訊操作(channel的傳送和接收);當通道接收時,可以根據執行塊決定是否宣告接收變數;當滿足條件時,select會通訊並執行case後的語句;這時其他通訊不會執行;一個沒有任何case的select語句寫作select{},標識永遠等待下去;
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
如果多個case同時就緒時,select會隨機地選擇一個執行,以保證channel執行公平;
default中設定其他操作都能處理的邏輯;
nil的channel表示永遠阻塞;select語句中操作nil的channel永遠都不會被執行到;可以通過nil來啟用或禁用case,實現額外事件超時和取消的邏輯;
goroutine退出
程式中我們需要當業務執行異常或某些原因中途退出goroutine執行,這就需要goroutine可以退出.但是go並沒有提供一個goroutine中終止另一個goroutine的方法,因為這會造成goroutine間的共享變數狀態不可控;
退出方式有如下幾種:
1 向abort的channel中傳送和goroutine同樣數量的退出訊息;這種做法很理想,因為訊息傳送接收都可能阻塞,同時goroutine的實際數量也無法準確統計;
2 廣播機制;消費掉所有channel傳送值並關閉channel;這樣操作channel之後的程式碼會立即被執行;
基於訊號管理:
適合協程具備層級關係的情形;主協程下有子協程,主協程傳送資訊,觸發子協程的關閉;
問題:
接收者和傳送者,缺少並行機制,訊息訂閱模式;
builtin包的close方法
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
close(done)
}()
建立一個goroutine,用於執行匿名函式,輸入任意字元觸發close方法,close可以接收一個傳送者通道,用於關閉通道並進行關閉廣播;它會向所有接收者傳送訊息,用於觸發接收者後面的程式碼;
當主協程返回時,一個程式會退出,這樣你無法在主函式退出後確認是否所有資源都進行了釋放;這裡可以使用panic讓runtime把每個goroutine的棧dump下來,如果主協程時唯一剩下的goroutine,證明資源釋放成功,否則就想辦法進行資源釋放;
核心問題:呼叫鏈關係.
資源請求和管理有層級關係;
全域性機制:上下文共享資料結構,只讀,規避風險;具備通過具體點進行上下的層管理.