1. 程式人生 > >淺談Go語言的Goroutine和協程

淺談Go語言的Goroutine和協程

0x00.前言

前面寫了一篇初識Go語言和大家一起學習了Go語言的巨大潛力、語言簡史、殺手鐗特性等,感興趣的讀者可以回顧一下。

今天來學習Go語言的Goroutine機制,這也可能是Go語言最為吸引人的特性了,理解它對於掌握Go語言大有裨益,話不多說開始吧!

通過本文你將瞭解到以下內容:

  • 什麼是協程以及橫向對比優勢
  • Go語言的Goroutine機制底層原理和特點

0x01.聊聊協程

大家對於程序、執行緒二位明星都很熟悉,但協程就沒有火了,是協程不是攜程哦!

協程並不是Go語言特有的機制,相反像Lua、Ruby、Python、Kotlin、C/C++等也都有協程的支援,區別在於有的是從語言層面支援、有的通過外掛類庫支援。Go語言是原生語言層面支援,本文也是從Go角度去理解協程。

1.1 協程基本概念和提出者

協程英文是Coroutine譯為協同程式,我們來看下維基百科對Coroutine的介紹:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.
Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.
According to Donald Knuth, Melvin Conway coined the term coroutine in 1958 when he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963.

簡單翻譯一下:

協同程式是一種計算機程式元件,它允許暫停和恢復執行,從而可以作為通用化的非搶佔式多工處理子程式。
協同程式非常適合實現例如協作任務、異常、事件迴圈、迭代器、管道等熟悉的程式元件。
根據唐納德·克努特的說法,梅爾文·康威在1958年將Coroutine這個術語應用於裝配程式的構建,直到在1963年才首次發表了闡述Coroutine的論文。

協程的提出者梅爾文·愛德華·康威是一位電腦科學家,除了協程之外他還創造了Conway's Law康威定律,他基於社會學觀察提出了系統設計的一些觀點,本文就不展開了,感興趣的可以看下作者的論文How Do Committees Invent?:

http://www.melconway.com/Home/Committees_Paper.html

1.2 協程和進執行緒的對比

我們來複習一下進執行緒和協程的一些基本特點吧:

程序是系統資源分配的最小單位, 程序包括文字段text region、資料段data region和堆疊段stack region等。程序的建立和銷燬都是系統資源級別的,因此是一種比較昂貴的操作,程序是搶佔式排程其有三個狀態:等待態、就緒態、執行態。程序之間是相互隔離的,它們各自擁有自己的系統資源, 更加安全但是也存在程序間通訊不便的問題。

程序是執行緒的載體容器,多個執行緒除了共享程序的資源還擁有自己的一少部分獨立的資源,因此相比程序而言更加輕量,程序內的多個執行緒間的通訊比程序容易,但是也同樣帶來了同步和互斥的問題和執行緒安全問題,儘管如此多執行緒程式設計仍然是當前服務端程式設計的主流,執行緒也是CPU排程的最小單位,多執行緒執行時就存線上程切換問題,其狀態轉移如圖:

協程在有的資料中稱為微執行緒或者使用者態輕量級執行緒,協程排程不需要核心參與而是完全由使用者態程式來決定,因此協程對於系統而言是無感知的。協程由使用者態控制就不存在搶佔式排程那樣強制的CPU控制權切換到其他進執行緒,多個協程進行協作式排程,協程自己主動把控制權轉讓出去之後,其他協程才能被執行到,這樣就避免了系統切換開銷提高了CPU的使用效率。

搶佔式排程和協作式排程的簡單對比:

看到這裡我們不免去想:看著協作式排程優點更多,那麼為什麼一直是搶佔式排程佔上風呢?讓我們繼續一起學習,可能就能解答這個問題了。

1.3 實際工作中的我們

我們寫程式的時經常需要考慮的因素就是提高機器使用率,這個非常好理解。當然機器使用率和開發效率維護成本往往存在權衡,說句大白話就是:要麼費人力要麼費機器,選一個吧!

機器成本里面最貴的就是CPU了,程式一般分為CPU密集型和IO密集型,對於CPU密集型我們的優化空間可能沒那麼多,但對於IO密集型卻有非常大的優化空間,試想我們的程式總是處於IO等待中讓CPU呼呼睡大覺,那該多糟糕。

為了提高IO密集型程式的CPU使用率,我們嘗試多程序/多執行緒程式設計等讓多個任務一起跑分時複用搶佔式排程,這樣提高了CPU的利用率,但由於多個進執行緒存在排程切換,這也有一定的資源消耗,因此進執行緒數量不可能無限增大。

我們現在寫的程式大部分都是同步IO的,效率還不夠高,因此出現了一些非同步IO框架,但是非同步框架的程式設計難度比同步框架要大,但不可否認非同步是一個很好的優化方向,先不要暈,來看下同步IO和非同步IO就知道了:

同步是指應用程式發起I/O請求後需要等待或者輪詢核心I/O操作完成後才能繼續執行,非同步是指應用程式發起I/O請求後仍繼續執行,當核心I/O操作完成後會通知應用程式或者呼叫應用程式註冊的回撥函式。

我們以C/C++開發的服務端程式為例,Linux的非同步IO出現的比較晚,因此像epoll之類的IO複用技術仍然有相當大的地盤,但是同步IO的效率畢竟不如非同步IO,因此當前的優化方向包括:非同步IO框架(像boost.asio框架)和協程方案(騰訊libco)。

0x02.Go和協程

我們知道協程是Coroutine,Go語言在語言層面對協程進行了原生支援並且稱之為Goroutine,這也是Go語言強大併發能力的重要支撐,Go的CSP併發模型是通過Goroutine和channel來實現的,後續會專門寫一下CSP併發模型。

2.1 協作式排程和排程器

協作式排程中使用者態協程會主動讓出CPU控制權來讓其他協程使用,確實提高了CPU的使用率,但是不由得去思考使用者態協程不夠智慧怎麼辦?不知道何時讓出控制權也不知道何時恢復執行。

讀到這裡忽然明白了搶佔式排程的優勢了,在搶佔式排程中都是由系統核心來完成的,使用者態不需要參與,並且核心參與使得平臺移植好,說到底還是各有千秋啊!

為了解決這個問題我們需要一箇中間層來排程這些協程,這樣才能讓使用者態的成千上萬個協程穩定有序地跑起來,我們姑且把這個中間層稱為使用者態協程排程器吧!

2.2 Goroutine和Go的排程器模型

Go語言從2007年底開發直到今天已經發展了12年,Go的排程器也不是一蹴而就的,在最初的幾個版本中Go的排程器也非常簡陋,無法支撐大併發。

經過多個版本的迭代和優化,目前已經有很優異的效能了,不過我們還是來回顧一下Go排程器的發展歷程(詳見參考一):

圖片來自網路

Go的排程器非常複雜,篇幅所限本文只提一些基本的概念和原理,後續會深入去展開Go的排程器。

最近幾個版本的Go排程器採用GPM模型,其中有幾個概念先看下:

圖片來自網路

GPM模型使用一種M:N的排程器來排程任意數量的協程運行於任意數量的系統執行緒中,從而保證了上下文切換的速度並且利用多核,但是增加了排程器的複雜度。

來看兩張圖來進一步理解一下:

圖片來自網路

整個GPM排程的簡單過程如下:

新建立的Goroutine會先存放在Global全域性佇列中,等待Go排程器進行排程,隨後Goroutine被分配給其中的一個邏輯處理器P,並放到這個邏輯處理器對應的Local本地執行佇列中,最終等待被邏輯處理器P執行即可。
在M與P繫結後,M會不斷從P的Local佇列中無鎖地取出G,並切換到G的堆疊執行,當P的Local佇列中沒有G時,再從Global佇列中獲取一個G,當Global佇列中也沒有待執行的G時,則嘗試從其它的P竊取部分G來執行相當於P之間的負載均衡。

Goroutine在整個生存期也存在不同的狀態切換,主要的有以下幾種狀態:

畫個狀態圖看下:

0x03.巨人的肩膀

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/

https://www.flysnow.org/2017/04/11/go-in-action-go-goroutine.htm

人類身份驗證 - SegmentFault

https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.2.html

https://wudaijun.com/2018/01/go-scheduler/

就想叫yoko:[譯] Go語言調