1. 程式人生 > >程序、執行緒、協程、goroutine區別

程序、執行緒、協程、goroutine區別

        在golang開發的過程中相信大家最經常接觸的就是go協程,但對於什麼是協程以及什麼是go協程,可能還停留在go出去的就是協程這個表面認知,這不僅會給我們專案帶來隱藏的問題。對此,結合一些資料,從作業系統的角度來對程序,執行緒,協程進行介紹,並試著說明協程和goruntine是什麼。

一、概念理解

  1、程序

        程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,程序由作業系統排程。不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制代碼等)比較大,但相對比較穩定安全。

  2、執行緒

        執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,而擁有自己獨立的棧和共享的堆,共享堆,不共享棧,執行緒也由作業系統排程(標準執行緒是這樣的)。只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。

  3、協程

       協程是一種使用者態的輕量級執行緒,

協程的排程完全由使用者控制。協程和執行緒一樣共享堆,不共享棧,協程由程式設計師在協程的程式碼裡顯示排程。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快。

   

        一個應用程式一般對應一個程序,一個程序一般有一個主執行緒,還有若干個輔助執行緒,執行緒之間是平行執行的,線上程裡面可以開啟協程,讓程式在特定的時間內執行。

 

二、理解區分

      

需要區分程序、執行緒(核心級執行緒)、協程(使用者級執行緒)三個概念。

 (1)程序、執行緒 和 協程 之間概念的區別

  對於 程序執行緒,都是有核心進行排程,有 CPU 時間片的概念,進行 搶佔式排程(有多種排程演算法)

   對於 協程(使用者級執行緒),這是對核心透明的,也就是系統並不知道有協程的存在,是完全由使用者自己的程式進行排程的,因為是由使用者程式自己控制,那麼就很難像搶佔式排程那樣做到強制的 CPU 控制權切換到其他程序/執行緒,通常只能進行 協作式排程,需要協程自己主動把控制權轉讓出去之後,其他協程才能被執行到。對於執行緒,每個執行緒都有一個id,這個線上程建立時就會返回,所以可以很方便的通過id操作某個執行緒。但是在goroutine內沒有這個概念,這個是go語言設計之初考慮的,防止被濫用,不能在一個協程中殺死另外一個協程,編碼時需要考慮到協程什麼時候建立,什麼時候釋放。

 (2)goroutine 和協程區別

  本質上,goroutine 就是協程。 不同的是,Golang 在 runtime、系統呼叫等多方面對 goroutine 排程進行了封裝和處理,當遇到長時間執行或者進行系統呼叫時,會主動把當前 goroutine 的CPU (P) 轉讓出去,讓其他 goroutine 能被排程並執行,也就是 Golang 從語言層面支援了協程。Golang 的一大特色就是從語言層面原生支援協程,在函式或者方法前面加 go關鍵字就可建立一個協程。

 (3)其他方面的比較

  1. 記憶體消耗方面

    每個 goroutine (協程) 預設佔用記憶體遠比 Java 、C 的執行緒少。
    goroutine:2KB 
    執行緒:8MB

  2. 執行緒和 goroutine 切換排程開銷方面

    執行緒/goroutine 切換開銷方面,goroutine 遠比執行緒小
    執行緒:涉及模式切換(從使用者態切換到核心態)、16個暫存器、PC、SP...等暫存器的重新整理等。
    goroutine:只有三個暫存器的值修改 - PC / SP / DX.   

三、goroutine協程

  1. goroutine協程實現原理

       執行緒是作業系統的核心物件,多執行緒程式設計時,如果執行緒數過多,就會導致頻繁的上下文切換,這些 cpu 時間是一個額外的耗費。所以在一些高併發的網路伺服器程式設計中,使用一個執行緒服務一個 socket 連線是很不明智的。於是作業系統提供了基於事件模式的非同步程式設計模型。用少量的執行緒來服務大量的網路連線和I/O操作。但是採用非同步和基於事件的程式設計模型,複雜化了程式程式碼的編寫,非常容易出錯。因為執行緒穿插,也提高排查錯誤的難度。

       協程,是在應用層模擬的執行緒,他避免了上下文切換的額外耗費,兼顧了多執行緒的優點。簡化了高併發程式的複雜度。舉個例子,一個高併發的網路伺服器,每一個socket連線進來,伺服器用一個協程來對他進行服務。程式碼非常清晰。而且兼顧了效能。

  協程和執行緒的原理是一樣的,當 a執行緒 切換到 b執行緒 的時候,需要將 a執行緒 的相關執行進度壓入棧,然後將 b執行緒 的執行進度出棧,進入 b執行緒 的執行序列。協程只不過是在 應用層 實現這一點。但是,協程並不是由作業系統排程的,而且應用程式也沒有能力和許可權執行 cpu 排程。怎麼解決這個問題?

  協程是基於執行緒的。內部實現上,維護了一組資料結構和 n 個執行緒,真正的執行還是執行緒,協程執行的程式碼被扔進一個待執行佇列中,由這 n 個執行緒從佇列中拉出來執行。這就解決了協程的執行問題。那麼協程是怎麼切換的呢?答案是:golang 對各種 io函式 進行了封裝,這些封裝的函式提供給應用程式使用,而其內部呼叫了作業系統的非同步 io函式,當這些非同步函式返回 busy 或 bloking 時,golang 利用這個時機將現有的執行序列壓棧,讓執行緒去拉另外一個協程的程式碼來執行,基本原理就是這樣,利用並封裝了作業系統的非同步函式。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。

   由於golang是從編譯器和語言基礎庫多個層面對協程做了實現,所以,golang的協程是目前各類有協程概念的語言中實現的最完整和成熟的。十萬個協程同時執行也毫無壓力。關鍵我們不會這麼寫程式碼。但是總體而言,程式設計師可以在編寫 golang 程式碼的時候,可以更多的關注業務邏輯的實現,更少的在這些關鍵的基礎構件上耗費太多精力。

  2. goroutine排程

    執行緒切換需要陷入核心,然後進行上下文切換,而協程在使用者態由協程排程器完成,不需要陷入核心,這代價就小了;另外,協程的切換時間點是由排程器決定的,而不是系統核心決定的,儘管他們切換點都是時間片超過一定閾值,或者進入I/O或睡眠等狀態;再次,還有垃圾回收的考慮,因為go實現了垃圾回收,而垃圾回收的必要條件時記憶體位於一致狀態,這就需要暫停所有的執行緒,如果交給系統去做,那麼會暫停所有的執行緒使其一致,而在go裡面排程器知道什麼時候記憶體位於一致狀態,那麼就沒有必要暫停所有執行的協程。
  對執行緒來說,有三種對映(使用者執行緒與核心執行緒的因素)模型:

  • 一對一模型(1:1)。一個使用者執行緒對映到一個核心執行緒,使用者執行緒在存活期都會繫結到一個核心執行緒,一旦退出,2個執行緒都會退出。優點是實現了真正的併發,多個執行緒同時跑在不同的CPU上;缺點是,如果使用者執行緒起多了,核心執行緒肯定不夠用,那麼就需要切換,涉及到上下文的切換,代價比較大。
  • 多對一模型(M:1)。多個使用者執行緒對映到一個核心執行緒。優點是,多個使用者執行緒切換比較快,不需要核心執行緒上下文切換;缺點是,如果一個執行緒阻塞了,那麼對映到同一個核心執行緒的使用者執行緒將都無法執行。
  • 多對多模型(M:N)。綜合以上兩種模型,go採用的就是這種。下面進行具體介紹。

  go排程裡面有三個角色:三角形M代表核心執行緒,正方形P代表上下文,圓形G代表協程: goroutine1
下面圖我們看到他們之間的對應規則:一個M對應一個P,一個P下面掛多個G,但一個時候只有一個G在跑,其餘都是放入等待佇列,等待下一次切換時使用。
goroutine2
  那麼假如一個執行的協程G呼叫syscall進入阻塞怎麼辦?如下圖左邊,G0進入阻塞,那麼P會轉移到另外一個核心執行緒M1(此時還是1對1)。當syscall返回後,需要搶佔一個P繼續執行,如果搶佔不到,G0掛入全域性就緒佇列runqueue,等待下次排程,理論上會被掛入到一個具體P下面的就緒佇列runqueu(區別於全域性runqueue)。
goroutine3
  假如一個P0下面的所有G都跑完了,怎麼辦?這時候會從別的P1下面就緒佇列搶佔G進行執行,個數為P1就緒佇列的一半。
goroutine4

 

四、個人總結

          總體上,協程與執行緒主要區別是它將不再被核心排程,而是交給了程式自己而執行緒是將自己交給核心排程,所以也不難理解golang中排程器的存在。協程避免了無意義的排程,由此可以提高效能,但也因此,程式設計師必須自己承擔排程的責任,同時,協程也失去了標準執行緒使用多CPU的能力。協程的概念並不是與執行緒對應的,應該說和函式呼叫 call/return對應(不難理解為什麼會把golang中的goruntine當作一個以函式為單位的執行單元)。它們的區別在於協程允許一個函式有多個入口、出口(邏輯上的),並且在切換到另一個函式執行時,允許使用一個新的context(包括呼叫棧)。正是有了這個機制基礎,再加上CPU支援了保護模式,作業系統就可以接著實現程序、執行緒了。
         

       明白了協程原理,程序和執行緒就比較好理解了。個人覺得程序與執行緒其實最核心的是隔離與並行。程序可看作為分配資源的基本單位,比如new出了一塊記憶體,就是作業系統將一塊實體記憶體對映到你的程序地址空間上(程序建立必須分配一個完整的獨立地址空間),這塊記憶體就屬於這個程序,程序內的所有執行緒都可以訪問這塊記憶體,其他程序就訪問不了,其他型別的資源也是同理。所以程序是分配資源的基本單位,也是我們說的隔離執行緒作為獨立執行和獨立排程的基本單位,進而我們可以認為執行緒是程序的一個執行流,獨立執行它自己的程式程式碼。執行緒上下文一般只包含CPU上下文及其他的執行緒管理資訊,執行緒建立的開銷主要取決於為執行緒堆疊的建立而分配記憶體的開銷,這些開銷並不大。執行緒還分為系統級別和使用者級執行緒,使用者級別執行緒對引起阻塞的系統呼叫的呼叫會立即阻塞該執行緒所屬的整個程序,而核心實現執行緒則會導致執行緒上下文切換的開銷跟程序一樣大,所以經常的折衷的方法是輕量級程序(Lightweight)。在 Linux 中,一個執行緒組基本上就是實現了多執行緒應用的一組輕量級程序。執行緒的作用就在於充分使用硬體CPU,也就是我們說的並行。
       

       從應用角度來說,我們一般將協程理解為使用者態輕量級執行緒,是對核心透明的,也就是系統並不知道有協程的存在,是完全由使用者的程式自己排程的,因為是由使用者程式自己控制,那麼就很難像搶佔式排程那樣做到強制的CPU控制權切換到其他程序/執行緒,通常只能進行協作式排程,需要協程自己主動把控制權轉讓出去之後,其他協程才能被執行到。但我們以上說的協程和golang中的協程是不一樣的。就像開頭說的很多人將go的協程理解為我們常說的協程,但深究它們的名稱不難看出,一個是goruntine,另一個是Coroutine,是不一樣的。golang語言作者Rob Pike也說,“Goroutine是一個與其他goroutines 併發執行在同一地址空間的Go函式或方法。一個執行的程式由一個或更多個goroutine組成。它與執行緒、協程、程序等不同。它是一個goroutine“。 Go 協程意味著並行,協程一般來說不是這樣的;Go 協程通過通道來通訊而協程通過讓出和恢復操作來通訊;而且Go 協程比協程更強大。因為Golang 在 runtime、系統呼叫等多方面對 goroutine 排程進行了封裝和處理,也就是Golang 有自己的排程器,工作方式基本上是協作式,而不是搶佔式,但也不是完全的協作式排程,例如在系統呼叫的函式入口處會有搶佔。當遇到長時間執行或者進行系統呼叫時,會主動把當前 goroutine 的CPU (P) 轉讓出去,讓其他 goroutine 能被排程並執行,也就是我們為什麼說 Golang 從語言層面支援了協程。簡單的說就是golang自己實現了協程並叫做goruntine
       

        在golang中程序和執行緒概念基本和我們常說的一致,大多呼叫系統的API實現,例如os 包及其子包 os/exec 提供了建立程序的方法,在 Unix 中,建立一個程序,通過系統呼叫 fork 實現(及其一些變種,如 vfork、clone),在windows中通過系統呼叫CreateProcess等。相信熟悉golang的都用過GOMAXPROCS,很多人都簡單地理解為這個是限制程序數量,這樣理解顯然不僅是望文生義還有就是對程序和執行緒理解不夠,官方解釋就很準確: GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously。很清楚,就是限制cpu數,限制cpu數,本質上是什麼,就是限制並行數,並行數即同時執行數量,執行單元即執行緒,即限制最大並行執行緒數量。
       

       goruntine的優勢在於並行和非常低的資源使用,體現在記憶體消耗方面和切換(排程)開銷方面,每個 goroutine (協程) 預設佔用記憶體遠比 Java 、C 的執行緒少,只有2KB,而執行緒則需要8MB;執行緒切換涉及模式切換(從使用者態切換到核心態)、16個暫存器、PC、SP...等暫存器的重新整理等;而goroutine 只有三個暫存器的值修改 - PC / SP / DX。