現在的位置: 首頁 > 編程語言 > 正文

Go語言的Goroutine和協程是什麼

2020年02月20日 編程語言 ⁄ 共 2620字 ⁄ 字型大小 評論關閉

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

  1.協程簡介

  大家對於進程、線程都很熟悉,但協程就沒有火了,協程並不是Go語言特有的機制,相反像Lua、Ruby、Python、Kotlin、C/C++等也都有協程的支持,區別在於有的是從語言層面支持、有的通過插件類庫支持。Go語言是原生語言層面支持,本文也是從Go角度去理解協程。

  1.1 協程基本概念和提出者

  協程英文是Coroutine譯為協同程序「

  協同程序是一種計算機程序組件,它允許暫停和恢復執行,從而可以作為通用化的非搶佔式多任務處理子程序。

  協同程序非常適合實現例如協作任務、異常、事件循環、迭代器、管道等熟悉的程序組件。

  根據唐納德·克努特的說法,梅爾文·康威在1958年將Coroutine這個術語應用於裝配程序的構建,直到在1963年才首次發表了闡述Coroutine的論文。

  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)。

  2.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之間的負載均衡。

抱歉!評論已關閉.