golang併發之Goroutine & channel完全解讀

目錄

0、幾個概念

1、一個通道發送和接收數據,默認是阻塞的。

2、關閉通道和通道上範圍循環

(1)關閉通道

(2)通道上範圍循環遍歷

3、緩衝通道

(1)非緩衝通道

(2)緩衝通道

4、定向通道

(1)單向 channel 變量的聲明

(2)time包中的單向通道相關函數

5、Select語句

6、CSP併發模型

7、併發編程實例

(1)請完成goroutine和channel協調工作的案例

(2)統計素數

(3)計算累加和


0、幾個概念

(1)進程/線程

  • 進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。
  • 線程是進程的一個執行實體,是 CPU 調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位
  • 一個進程可以創建和撤銷多個線程,同一個進程中的多個線程之間可以併發執行。

(2)併發/並行

  • 多線程程序在單核的 cpu 上運行,稱爲併發;多線程程序在多核的 cpu 上運行,稱爲並行。
  • 併發與並行並不相同,併發主要由切換時間片來實現“同時”運行,並行則是直接利用多核實現多線程的運行,Go程序可以設置使用核心數,以發揮多核計算機的能力。

(3)協程/線程

  • 協程:獨立的棧空間,共享堆空間,調度由用戶自己控制,本質上有點類似於用戶級線程,這些用戶級線程的調度也是自己實現的。
  • 線程:一個線程上可以跑多個協程,協程是輕量級的線程
  • 優雅的併發編程範式,完善的併發支持,出色的併發性能是Go語言區別於其他語言的一大特色。使用Go語言開發服務器程序時,就需要對它的併發機制有深入的瞭解。

(4)Goroutine

  • goroutine 是一種非常輕量級的實現,可在單個進程裏執行成千上萬的併發任務,它是Go語言併發設計的核心。
  • 說到底 goroutine 其實就是線程,但是它比線程更小,十幾個 goroutine 可能體現在底層就是五六個線程,而且Go語言內部也實現了 goroutine 之間的內存共享。
  • 使用 go 關鍵字就可以創建 goroutine,將 go 聲明放到一個需調用的函數之前,在相同地址空間調用運行這個函數,這樣該函數執行時便會作爲一個獨立的併發線程,這種線程在Go語言中則被稱爲 goroutine。

(5)channel

  • channel 是Go語言在語言級別提供的 goroutine 間的通信方式。可以使用 channel 在兩個或多個 goroutine 之間傳遞消息

1、一個通道發送和接收數據,默認是阻塞的。

  • 如果只有寫數據,會發生死鎖;如果只有讀數據,也會發生死鎖。
  • 在沒有使用協程的情況下,如果我們的管道數據已經全部取出,再取就會報fatal error: all goroutines are asleep - deadlock!
  • 當我們給管道寫入數據時,不能超過其容量

2、關閉通道和通道上範圍循環

使用內置函數close可以關閉channel,當channel關閉後,就不能再向channel寫數據了,但是仍然可以從該channel讀取數據

(1)關閉通道

close的目的就是用來通知對方這個通道是關閉的,以此來結束循環

關閉 channel 非常簡單,直接使用 Go語言內置的 close() 函數即可:

close(ch)

在介紹瞭如何關閉 channel 之後,我們就多了一個問題:如何判斷一個 channel 是否已經被關閉?我們可以在讀取的時候使用多重返回值的方式:

x, ok := <-ch

這個用法與 map 中的按鍵獲取 value 的過程比較類似,只需要看第二個 bool 返回值即可,如果返回值是 false 則表示 ch 已經被關閉。

(2)通道上範圍循環遍歷

管道不能使用普通的for循環

在遍歷時,如果channel沒關閉,則會出現deadlock的錯誤:fatal error: all goroutines are asleep - deadlock!

在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完後,就會退出遍歷

3、緩衝通道

(1)非緩衝通道

以上介紹的通道都沒有緩衝,發送和接收到一個未緩衝的通道是阻塞的。

一次發送操作對應一次接收操作,對於一個goroutine來講,它的一次發送,在另一個goroutine接收之前都是阻塞的。同樣的,對於接收來講,在另一個goroutine發送之前,它也是阻塞的

(2)緩衝通道

緩衝通道就是指一個通道帶有緩衝區。發送到一個緩衝通道只有在緩衝區滿時才被阻塞。類似地,從緩衝通道接收的信息只有在緩衝區爲空時纔會被阻塞。帶緩衝通道在很多特性上和無緩衝通道是類似的。無緩衝通道可以看作是長度永遠爲 0 的帶緩衝通道。

因此根據這個特性,帶緩衝通道在下面列舉的情況下依然會發生阻塞

  • 帶緩衝通道被填滿時,嘗試再次發送數據時發生阻塞。
  • 帶緩衝通道爲空時,嘗試接收數據時發生阻塞。

如何創建帶緩衝的通道呢——通道實例 := make(chan 通道類型, 緩衝大小)

  • 通道類型:和無緩衝通道用法一致,影響通道發送和接收的數據類型。
  • 緩衝大小:決定通道最多可以保存的元素數量。
  • 通道實例:被創建出的通道實例。

爲什麼Go語言對通道要限制長度而不提供無限長度的通道?

我們知道通道(channel)是在兩個 goroutine 間通信的橋樑。使用 goroutine 的代碼必然有一方提供數據,一方消費數據。當提供數據一方的數據供給速度大於消費方的數據處理速度時,如果通道不限制長度,那麼內存將不斷膨脹直到應用崩潰。因此,限制通道的長度有利於約束數據提供方的供給速度,供給數據量必須在消費方處理量+通道長度的範圍內,才能正常地處理數據。

4、定向通道

我們在將一個 channel 變量傳遞到一個函數時,可以通過將其指定爲單向 channel 變量,從而限制該函數中可以對此 channel 的操作,比如只能往這個 channel 寫,或者只能從這個 channel 讀。

(1)單向 channel 變量的聲明

單向 channel 變量的聲明非常簡單,只能發送的通道類型爲chan<-,只能接收的通道類型爲<-chan,格式如下:

  • var 通道實例 chan<- 元素類型    // 只能發送通道
  • var 通道實例 <-chan 元素類型    // 只能接收通道

(2)time包中的單向通道相關函數

time 包中的計時器會返回一個 timer 實例,代碼如下:

timer := time.NewTimer(time.Second)

timer的Timer類型定義如下:

type Timer struct {
  C <-chan Time
  r runtimeTimer
}

第 2 行中 C 通道的類型就是一種只能接收的單向通道。如果此處不進行通道方向約束,一旦外部向通道發送數據,將會造成其他使用到計時器的地方邏輯產生混亂。因此,單向通道有利於代碼接口的嚴謹性

NewTimer和After方法差不多,只是返回值不同;After直接返回通道

5、Select語句

select 的用法與 switch 語言非常類似,由 select 開始一個新的選擇塊,每個選擇條件由 case 語句來描述。與 switch 語句相比,select 有比較多的限制,其中最大的一條限制就是每個 case 語句裏必須是一個 IO 操作,select會隨機執行一個可運行的case。如果沒有case可運行,它將阻塞,直到case可運行。

在一個 select 語句中,Go語言會按順序從頭至尾評估每一個發送和接收的語句。
如果其中的任意一語句可以繼續執行(即沒有被阻塞),那麼就從那些可以執行的語句中任意選擇一條來使用。
如果沒有任意一條語句可以執行(即所有的通道都被阻塞),那麼有如下兩種可能的情況:

  • 如果給出了 default 語句,那麼就會執行 default 語句,同時程序的執行會從 select 語句後的語句中恢復;
  • 如果沒有 default 語句,那麼 select 語句將被阻塞,直到至少有一個通信可以進行下去。

6、CSP併發模型

CSP 併發模型是上個世紀七十年代提出的,用於描述兩個獨立的併發實體通過共享 channel(管道)進行通信的併發模型。但是Go語言並沒有完全實現了 CSP 併發模型的所有理論,僅僅是實現了 process 和 channel 這兩個概念。process 就是Go語言中的 goroutine,每個 goroutine 之間是通過 channel 通訊來實現數據共享。

7、併發編程實例

(1)請完成goroutine和channel協調工作的案例

具體要求:

  • 開啓一個writeData協程,向管道intChan中寫入50個整數
  • 開啓一個readChan協程,從管道中讀取writeData寫入的數據
  • 注意:writeData和readChan操作的是同一個管道
  • 主線程需要等待writeData和readData協程都完成工作才能退出【管道】

以下兩種寫法均可

   

或者:

   

(2)統計素數

  • 要求統計1-20000的數字中,哪些是素數?將統計素數的任務分給4個協程去完成

(3)計算累加和

  • 啓動一個協程,將1-2000的數放到一個channel中,比如numChan
  • 啓動8個協程,從numChan取出數字(比如n),並計算1+2+……+n的值,並存放到resChan
  • 最後8個協程同完成工作後,再遍歷resChan,顯示結果【如res[1]=1……res[10]=55……】

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章