剛送完 《深入剖析Kubernetes》,今天又來送《Go 語言零基礎入門到項目實戰》。。


什麼是併發?有哪些我們需要知道的併發模式?Go 語言中的協程併發模型是怎樣的?什麼是主 goroutine?它與我們自己啓用的其他 goroutine 有什麼不同?

本文就來爲你一一解答!

以下內容節選自《Go 語言極簡一本通:零基礎入門到項目實戰》一書!


併發

串行程序,即程序的執行順序和程序的編寫順序一致,整個程序只有一個上下文,就是一個棧,一個堆。

併發程序,則需要運行多個上下文,對應多個調用棧。每個進程在運行時,都有自己的調用棧和堆,有一套完整的上下文。操作系統在調用時,會保證被調度進程的上下文環境,待該進程獲得時間後,再將該進程的上下文恢復到系統中。

串行的代碼是逐行執行的,是確定的,而併發引入了不確定性。線程通信只能採用共享內存的方式,爲了保證共享內存的有效性,可以加鎖,但是這樣又引入了死鎖的風險。

併發的優勢如下:

(1)可以充分利用 CPU 核心的優勢,提高程序的執行效率。

(2)併發能充分利用 CPU 與其他硬件設備的異步性,如文件操作等。

下面介紹 3 種併發模式。

多進程是操作系統層面的併發模式

所有的進程都由內核管理。進程描述的是程序的執行過程,是運行着的程序。

一個進程其實就是一個程序運行時的產物。

電腦爲什麼可以同時運行那麼多應用程序?手機爲什麼可以有那麼多 App 同時在後臺刷新?

這是因爲在它們的操作系統之上有多個代表着不同應用程序的進程在同時運行。

操作系統會爲每個獨立的程序創建一個進程,進程可以裝下整個程序需要的資源。例如,程序執行的進度、執行的結果等,都可以放在裏面。在程序運行結束後,再把進程銷燬,然後運行下一個程序,週而復始。

進程在程序運行中是非常佔用資源的,無論是否會用到全部的資源,只要程序啓動了,就會被加載到進程中。

優勢是進程互不影響,劣勢是開銷非常大。

多線程屬於系統層面的併發模式,也是使用最多、最有效的一種模式

線程是在進程之內的,可以把它理解爲輕量級的進程。它可以被視爲進程中代碼的執行流程。這樣在處理程序的運行和記錄中間結果時,就可以使用更少的資源。待資源用完,線程就會被銷燬。

線程要比進程輕量級很多。一個進程至少包含一個線程。如果一個進程只包含一個線程,那麼它裏面的所有代碼都只會被串行地執行。

每個進程的第一個線程都會隨着該進程的啓動而被創建,它們被稱爲其所屬進程的主線程。同理,如果一個進程中包含多個線程,那麼其中的代碼就可以被併發地執行。

除進程的第一個線程外,其他的線程都是由進程中已存在的線程創建出來的。也就是說,主線程之外的其他線程都只能由代碼顯式地創建和銷燬。這需要我們在編寫程序時進行手動控制。

優勢是比進程開銷小一些,劣勢是開銷仍然較大。

goroutine

從本質上說,goroutine 是一種用戶態線程,不需要操作系統進行搶佔式調度。

在 Go 程序中,Go 語言的運行時系統會自動地創建和銷燬系統級的線程。

系統級線程指的是操作系統提供的線程,而對應的用戶級線程(goroutine)指的是架設在系統級線程之上的,由用戶(或者說我們編寫的程序)完全控制的代碼執行流程。

用戶級線程的創建、銷燬、調度、狀態變更,以及其中的代碼和數據都完全需要我們的程序自己去實現和處理,其優勢如下:

(1)因爲它們的創建和銷燬不需要通過操作系統去做,所以速度很快,可以提高任務併發性。編程簡單、結構清晰。

(2)由於不用操作系統去調度它們的運行,所以很容易控制,並且很靈活。

協程併發模型

在 Go 語言中,不要通過共享數據來通信,恰恰相反,要通過通信的方式來共享數據。

Go 語言不僅有 goroutine,還有強大的用來調度 goroutine、對接系統級線程的調度器。

調度器是 Go 語言運行時系統的重要組成部分,它主要負責統籌調配 Go 併發編程模型中的三個主要元素,即 G(goroutine 的縮寫)、P(processor 的縮寫)和 M(machine 的縮寫),如下圖所示。

其中,M 指的就是系統級線程。而 P 指的是一種可以引用若干個 G,且能夠使這些 G 在恰當的時機與 M 進行對接,並得到運行的中介。

從宏觀上說,由於 P 的存在,G 和 M 可以呈現出多對多的關係。當一個正在與某個 M 對接並運行着的 G,需要因某個事件(比如等待 I/O 或鎖的解除)而暫停運行時,調度器總會及時地發現,並把這個 G 與那個 M 分離開,以釋放計算資源供那些等待運行的 G 使用。

而當一個 G 需要恢復運行時,調度器又會盡快地爲它尋找空閒的計算資源(包括 M)並安排運行。另外,當 M 不夠用時,調度器會向操作系統申請新的系統級線程,而當某個 M 已無用時,調度器又會負責把它及時地銷燬。

程序中的所有 goroutine 也都會被充分地調度,其中的代碼也都會被併發地運行,即使 goroutine 數以十萬計,仍然可以如此。

什麼是主 goroutine?它與我們自己啓用的其他 goroutine 有什麼不同?

先來看下面的代碼:

package main

import "fmt"

func main() {
  for i := 0; i < 10; i++ {
    go func() {
      fmt.Println(i)
    }()
  }
}

這段代碼只在 main 函數中寫了一條 for 語句。這條 for 語句中的代碼會迭代運行 10 次,並有一個局部變量 i 表示當次迭代的序號,該序號是從 0 開始的。在這條 for 語句中僅有一條 Go 語句,在這條 Go 語句中也僅有一條語句,該語句調用了 fmt.Println 函數,想要打印出變量 i 的值。

這個程序很簡單,只有三條語句。這個程序被執行後,會打印出什麼內容呢?

答案是:大部分計算機執行後,屏幕上不會有任何內容被打印出來。

這是爲什麼呢?

一個進程總會有一個主線程,類似地,每一個獨立的 Go 程序在運行時也總會有一個主 goroutine。這個主 goroutine 會在 Go 程序的運行準備工作完成後被自動地啓用。

一般來說,每條 Go 語句都帶有一個函數調用,這個被調用的函數就是 Go 函數。而主 goroutine 的 Go 函數就是那個作爲程序入口的 main 函數。Go 函數執行的時間與其所屬的 Go 語句執行的時間不同。

如下圖所示,當程序執行到一條 Go 語句時,Go 語言的運行時系統會先試圖從某個空閒的 G 隊列中獲取一個 G(也就是 goroutine),只有在找不到空閒 G 的情況下它纔會去創建一個新的 G。

如果已經存在一個 goroutine,那麼已存在的 goroutine 總是會被優先複用。如果不存在,就去啓動另一個 goroutine。

在 Go 語言中,創建 G 的成本非常低。創建一個 G 並不需要像新建一個進程或者一個系統級線程那樣,必須通過操作系統的系統調用來完成,而是在 Go 語言的運行時系統內部就可以完全做到,一個 G 僅相當於爲需要併發執行代碼片段服務的上下文環境。

在拿到一個空閒的 G 之後,Go 語言運行時系統會用這個 G 去包裝當前的那個 Go 函數(或者一個匿名的函數),然後再把這個 G 追加到某個可運行的 G 隊列中。隊列中的 G 總是按照先入先出的順序,由運行時系統安排運行。

由於上面所說的那些準備工作是不可避免的,所以會消耗一定時間。因此,Go 函數的執行時間總是慢於它所屬的 Go 語句的執行時間。

明白了這些之後,再來看上面的例子。請記住,只要 Go 語句本身執行完畢,Go 程序不會等待 Go 函數的執行,它就會立刻執行後邊的語句,這就是異步併發執行。

這裏“後邊的語句”一般指的是上面例子中 for 語句中的下一個迭代。當最後一個迭代運行時,這個“後邊的語句”是不存在的。

上面的那條 for 語句會以很快的速度執行完畢。當它執行完畢時,那 10 個包裝了 Go 函數的 goroutine 往往還沒有獲得運行的機會。Go 函數中的那個對 fmt.Println 函數的調用是以 for 語句中的變量 i 作爲參數的。

當 for 語句執行完畢時,這些 Go 函數都還沒有執行,那麼它們引用的變量 i 是多少呢?

一旦主 goroutine 中的代碼(也就是 main 函數中的那些代碼)執行完畢,當前的 Go 程序就會結束運行。當 Go 程序結束運行時,無論其他的 goroutine 是否運行,都不會被執行了。當 for 語句的最後一個迭代運行時,其中的那條 Go 語句即最後一條語句。所以,在執行完這條 Go 語句之後,主 goroutine 中的代碼就執行完了,Go 程序會立即結束運行。因此前面的代碼不會有任何內容被打印輸出。

嚴謹地講,Go 語言並不管這些 goroutine 以怎樣的順序運行。由於主 goroutine 會與我們自己啓用的其他 goroutine 一起被調度,而調度器很可能會在 goroutine 中的代碼只執行了一部分的時候暫停,以便所有的 goroutine 都有運行的機會。所以哪個 goroutine 先執行完,哪個 goroutine 後執行完往往是不可預知的。

對於上面簡單的代碼而言,絕大多數情況都是“不會有任何內容被打印出來”。但是爲了嚴謹起見,無論回答“打印出 10 個 10”,還是“不會有任何內容被打印出來”,或是“打印出亂序的 0 到 9”都是對的。

這個原理非常重要,希望讀者能理解。


《Go 語言極簡一本通:零基礎入門到項目實戰》

本書是一本 Go 語言入門書,全書共分爲三部分。

  • 第一部分講解 Go 語言基礎知識,包括變量與簡單類型、數組、切片、流程控制、字典、函數、結構體與方法、接口等,可以幫助讀者快速掌握 Go 語言的基本程序結構。
  • 第二部分講解 Go 語言高效併發相關知識,包括協程與通道、併發資源、包管理和測試等,讓讀者對 Go 語言層面的併發支持有更深入的理解。
  • 第三部分講解 Go 語言項目實戰,包括 Gin 框架、生活點評項目實戰、賬戶管理系統實戰,以及 OAuth 2.0 的授權協議等。通過實戰,把前面講解的知識點運用起來,幫助讀者快速上手,積累項目經驗。

贈書活動

關注公衆帳號雲原生實驗室,後臺回覆:夠浪獲取抽獎入口

👆關注公衆號,回覆 夠浪 即可參與

本次將隨機抽取5位,每人獲得《Go 語言極簡一本通:零基礎入門到項目實戰》書籍一本,感謝電子工業出版社提供的書籍

活動截止時間:4月19日18:00

沒抽中的小夥伴也可以通過如下入口直接購買~

點擊 "閱讀原文" 獲取更好的閱讀體驗!


發現朋友圈變“安靜”了嗎?


本文分享自微信公衆號 - 雲原生實驗室(cloud_native_yang)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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