go併發之goroutine和channel,併發控制入門篇

亦yi一發表於2020-12-15

併發的概念及其重要性

這段是簡單科普,大佬可以跳過

併發:併發程式指同時進行多個任務的程式。在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行,但任一個時刻點上只有一個程式在處理機上執行。

----------本段引用內容源自《GO語言高階程式設計》

在早期,CPU都是以單核的形式順序執行機器指令。Go語言的祖先C語言正是這種順序程式語言的代表。順序程式語言中的順序是指:所有的指令都是以序列的方式執行,在相同的時刻有且僅有一個CPU在順序執行程式的指令。

隨著處理器技術的發展,單核時代以提升處理器頻率來提高執行效率的方式遇到了瓶頸,目前各種主流的CPU頻率基本被鎖定在了3GHZ附近。單核CPU的發展的停滯,給多核CPU的發展帶來了機遇。相應地,程式語言也開始逐步向並行化的方向發展。Go語言正是在多核和網路化的時代背景下誕生的原生支援併發的程式語言。

在聊併發之前,聊聊共享變數、執行緒、協程

  1. 如何在不同執行緒/協程 共享 變數/記憶體?

這裡留給各位看官去自行查資料,即使我列出來也不如自己動手去查記憶深刻!

不想查也可以等我下一篇文章,更加詳細解讀執行緒、程式。

  1. 執行緒和協程概念?

執行緒:執行緒是作業系統能夠進行運算排程的最小單位。一個程式可以包含多個執行緒,是程式中的實際運作單位。

協程:又稱微執行緒。協程是一種使用者態的輕量級執行緒。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧。

  1. 為什麼會誕生協程?

雖然多執行緒在前網際網路世代已經足夠使用,但是執行緒的侷限性也比較明顯

  1. 執行緒數量有限,一般不會很多
  2. 執行緒佔據的資源通常比我們需要的多得多,造成浪費

每個系統級執行緒開闢都會佔用空間,這個空間可能是MB級別,但是我們如果使用的執行緒只需要傳遞KB級別資料,那麼執行緒看起來就會比較浪費,但是又不可避免。而且執行緒之間的切換也會佔用一些額外開銷。

為了解決上面的矛盾問題,協程誕生了:更小的資源開支,動態調配資源,比執行緒更輕量。

協程的一些優點:

  1. 因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。
  2. 不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。

在golang中,goroutine的建立消耗非常小,大約是KB級別。因此可以建立更多的協程,尤其是數量越多相對執行緒優勢更加明顯,而且goroutine可以動態伸縮,棧溢位風險也比執行緒更低。

golang的併發,goroutine的使用

var name = "yiyiyinhe"

func changeName() {
    name = "change"
}

func sayHi() {
    fmt.println("hi, ", name)
    go changeName() // 協程
}

簡單的協程就建立了,那麼列印出來的結果可能是hi, yiyiyinhe也可能是hi, change

如果想對某一程式碼塊執行協程而不是某個方法,則使用下面方式

var name = "yiyiyinhe"

func sayHi() {
    fmt.println("hi, ", name)
    go func() { // 匿名函式執行協程
        name = "change"
    }
}

channel

golang對共享變數的口號

Do not communicate by sharing memory; instead, share memory by communicating.

不要通過共享記憶體來通訊,而應通過通訊來共享記憶體。

那麼在協程中也需要進行通訊,而golang使用的goroutine之間通訊和同步的主要方法是channel。

什麼是channel呢?

A channel is a communic ation mechanism that lets one goroutine send values to another goroutine. Each channel is a conduit for values of a particular type, called the channel’s element type.

channel是一種通訊機制,它讓一個goroutine向另一個goroutine傳送值。每個通道都是特定型別(通道元素型別)值的管道。

簡單來理解就是,channel是用於在goroutine中進行通訊的管道,而且管道是有特定型別的。

建立的channel分為有快取和無快取兩種,區別就是建立的時候是否分配大小

  • 無快取channel

    • var ch1 = make(chan int),未分配大小
    • var ch2 = make(chan int, 0),分配大小為0也等同給於未分配大小
  • 有快取channel

    • var :ch3 = make(int, 3),分配大小為3的有快取channel

無快取channel中,channel的傳送操作總是在接收之前發生;簡單理解就是,無快取channel是一個管道必須從頭flag<-true傳送到尾部<-flag,而且尾部發生的時間一定是在頭部傳送之後。

  • 為什麼chennel可以這樣呢?

    因為channel有阻塞作用,必須接收了才能繼續下去。

channel

有快取channel則不具備上述的特性,因為對於帶緩衝的Channel,對於Channel的第 K 個接收完成操作發生在第 K+C 個傳送操作完成之前,其中 C 是Channel的快取大小。 如果將 C 設定為0自然就對應無快取的Channel,也即使第K個接收完成在第K個傳送完成之前。因為無快取的Channel只能同步發1個,也就簡化為前面無快取Channel的規則:對於從無緩衝Channel進行的接收,發生在對該Channel進行的傳送完成之前。

還是上面的例子,使用channel來演示一下

var name = "yiyi"
var flag = make(chan bool) // 建立了bool型別的channel

func changeName() {
    name = "change"
    flag <- true  // 傳送
}

func sayHi() {
    go changeName() // 協程
    <-flag // 接收
    fmt.println("hi, ", name)
}

那麼這個時候列印出來的就是一個固定的順序,由於<-flag接收總是在傳送之後執行,因此當flag <- true執行完之前name = "change"已經執行,列印結果一定是:hi, change

上面程式碼等同於下圖所示

看完你可以收穫什麼?

  1. 簡單瞭解併發,瞭解多執行緒簡單的發展來歷
  2. 簡單瞭解執行緒,協程
  3. 為什麼協程會誕生?
  4. goroutine的兩種使用方式
  5. channel是什麼?channel的兩種分類;
  6. channel在goroutine中有什麼作用?

寫在最後

由於我剛開始寫技術文章,很多東西不知道怎麼寫才能讓大家都看懂,就像寫執行緒協程的時候不知道要不要解釋共享變數或者共享記憶體是什麼,也不知道大家能不能知道多執行緒模式下執行緒之間通訊有哪些方式,感覺都想寫但是又覺得大家看文章標題應該是瞭解一些東西的,如果都寫篇幅太長,不寫又看不懂;就覺得比較矛盾吧,也希望大家能夠給我提一些意見建議!

作者還在慢慢努力,儘量把文章寫的通俗易懂,排版準確,抓住重點,把最好的內容展現給大家。

相關文章