Go語言是為并發而生的語言,Go語言是為數不多的在語言層面實現并發的語言;也正是Go語言的并發特性,吸引了全球無數的開發者。
并發(concurrency)和并行(parallellism)
-
并發(concurrency):兩個或兩個以上的任務在一段時間內被執行。我們不必care這些任務在某一個時間點是否是同時執行,可能同時執行,也可能不是,我們只關心在一段時間內,哪怕是很短的時間(一秒或者兩秒)是否執行解決了兩個或兩個以上任務。
-
并行(parallellism):兩個或兩個以上的任務在同一時刻被同時執行。
并發說的是邏輯上的概念,而并行,強調的是物理運行狀態。并發“包含”并行。
(詳情請見:Rob Pike 的PPT)
Go的CSP并發模型
Go實現了兩種并發形式。第一種是大家普遍認知的:多線程共享內存。其實就是Java或者C++等語言中的多線程開發。另外一種是Go語言特有的,也是Go語言推薦的:CSP(communicating sequential processes)并發模型。
CSP并發模型是在1970年左右提出的概念,屬于比較新的概念,不同于傳統的多線程通過共享內存來通信,CSP講究的是“以通信的方式來共享內存”。
請記住下面這句話:"Do not communicate by sharing memory; instead, share memory by communicating."“不要以共享內存的方式來通信,相反,要通過通信來共享內存。”
普通的線程并發模型,就是像Java、C++、或者Python,他們線程間通信都是通過共享內存的方式來進行的。非常典型的方式就是,在訪問共享數據(例如數組、Map、或者某個結構體或對象)的時候,通過鎖來訪問,因此,在很多時候,衍生出一種方便操作的數據結構,叫做“線程安全的數據結構”。例如Java提供的包”java.util.concurrent”中的數據結構。Go中也實現了傳統的線程并發模型。
Go的CSP并發模型,是通過goroutine
和channel
來實現的。
-
goroutine
是Go語言中并發的執行單位。有點抽象,其實就是和傳統概念上的”線程“類似,可以理解為”線程“。 -
channel
是Go語言中各個并發結構體(goroutine
)之前的通信機制。通俗的講,就是各個goroutine
之間通信的”管道“,有點類似于Linux中的管道。
生成一個goroutine
的方式非常的簡單:Go一下,就生成了:
gof();
通信機制channel
也很方便,傳數據用channel <- data
,取數據用<-channel
。
在通信過程中,傳數據channel <- data
和取數據<-channel
必然會成對出現,因為這邊傳,那邊取,兩個goroutine
之間才會實現通信。
而且不管傳還是取,必阻塞,直到另外的goroutine
傳或者取為止。
有兩個goroutine
,其中一個發起了向channel
中發起了傳值操作。(goroutine
為矩形,channel
為箭頭)
左邊的goroutine
開始阻塞,等待有人接收。
這時候,右邊的goroutine
發起了接收操作
右邊的goroutine
也開始阻塞,等待別人傳送。
這時候,兩邊goroutine
都發現了對方,于是兩個goroutine
開始一傳,一收。
這便是Golang CSP并發模型最基本的形式。
Go并發模型的實現原理
我們先從線程講起,無論語言層面何種并發模型,到了操作系統層面,一定是以線程的形態存在的。而操作系統根據資源訪問權限的不同,體系架構可分為用戶空間和內核空間;內核空間主要操作訪問CPU資源、I/O資源、內存資源等硬件資源,為上層應用程序提供最基本的基礎資源,用戶空間呢就是上層應用程序的固定活動空間,用戶空間不可以直接訪問資源,必須通過“系統調用”、“庫函數”或“Shell腳本”來調用內核空間提供的資源。
我們現在的計算機語言,可以狹義的認為是一種“軟件”,它們中所謂的“線程”,往往是用戶態的線程,和操作系統本身內核態的線程(簡稱KSE),還是有區別的。
線程模型的實現,可以分為以下幾種方式:
用戶級線程模型
如圖所示,多個用戶態的線程對應著一個內核線程,程序線程的創建、終止、切換或者同步等線程工作必須自身來完成。
內核級線程模型
這種模型直接調用操作系統的內核線程,所有線程的創建、終止、切換、同步等操作,都由內核來完成。C++就是這種。
兩級線程模型
這種模型是介于用戶級線程模型和內核級線程模型之間的一種線程模型。這種模型的實現非常復雜,和內核級線程模型類似,一個進程中可以對應多個內核級線程,但是進程中的線程不和內核線程一一對應;這種線程模型會先創建多個內核級線程,然后用自身的用戶級線程去對應創建的多個內核級線程,自身的用戶級線程需要本身程序去調度,內核級的線程交給操作系統內核去調度。
Go語言的線程模型就是一種特殊的兩級線程模型。暫且叫它“MPG”模型吧。
Go線程實現模型MPG
M
指的是Machine
,一個M直接關聯了一個內核線程。P
指的是”processor”,代表了M
所需的上下文環境,也是處理用戶級代碼邏輯的處理器。G
指的是Goroutine
,其實本質上也是一種輕量級的線程。
三者關系如下圖所示:
以上這個圖講的是兩個線程(內核線程)的情況。一個M會對應一個內核線程,一個M也會連接一個上下文P,一個上下文P相當于一個“處理器”,一個上下文連接一個或者多個Goroutine
。P(Processor)
的數量是在啟動時被設置為環境變量GOMAXPROCS
的值,或者通過運行時調用函數runtime.GOMAXPROCS()
進行設置。Processor
數量固定意味著任意時刻只有固定數量的線程在運行go代碼。Goroutine
中就是我們要執行并發的代碼。圖中P正在執行的Goroutine
為藍色的;處于待執行狀態的Goroutine
為灰色的,灰色的Goroutine
形成了一個隊列runqueues
。
三者關系的宏觀的圖為:
拋棄 P(Processor)
你可能會想,為什么一定需要一個上下文,我們能不能直接除去上下文,讓Goroutine
的runqueues
掛到M
上呢?答案是不行,需要上下文的目的,是讓我們可以直接放開其他線程,當遇到內核線程阻塞的時候。
一個很簡單的例子就是系統調用sysall
,一個線程肯定不能同時執行代碼和系統調用被阻塞,這個時候,此線程M需要放棄當前的上下文環境P
,以便可以讓其他的Goroutine
被調度執行。
如上圖左圖所示,M0
中的G0
執行了syscall
,然后就創建了一個M1
(也有可能本身就存在,沒創建),(轉向右圖)然后M0
丟棄了P
,等待syscall
的返回值,M1
接受了P
,將·繼續執行Goroutine
隊列中的其他Goroutine
。
當系統調用syscall
結束后,M0
會“偷”一個上下文,如果不成功,M0
就把它的Gouroutine
G0
放到一個全局的runqueue
中,然后自己放到線程池或者轉入休眠狀態。全局runqueue
是各個P
在運行完自己的本地的Goroutine runqueue
后用來拉取新goroutine
的地方。P
也會周期性的檢查這個全局runqueue
上的goroutine
,否則,全局runqueue
上的goroutines
可能得不到執行而餓死。
均衡的分配工作
按照以上的說法,上下文P
會定期的檢查全局的goroutine
隊列中的goroutine
,以便自己在消費掉自身Goroutine
隊列的時候有事可做。假如全局goroutine
隊列中的goroutine
也沒了呢?就從其他運行的中的P
的runqueue
里偷。
每個P
中的Goroutine
不同導致他們運行的效率和時間也不同,在一個有很多P和M的環境中,不能讓一個P
跑完自身的Goroutine
就沒事可做了,因為或許其他的P有很長的goroutine
隊列要跑,得需要均衡。該如何解決呢?
Go的做法倒也直接,從其他P
中偷一半!
原文標題:Golang 并發原理分析
文章出處:【微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
-
原理
+關注
關注
4文章
550瀏覽量
44895 -
模型
+關注
關注
1文章
3243瀏覽量
48836 -
go語言
+關注
關注
1文章
158瀏覽量
9049
原文標題:Golang 并發原理分析
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論