一文讀懂什麼是進程、線程、協程

進程

  我們都知道計算機的核心是CPU,它承擔了所有的計算任務;而操作系統是計算機的管理者,它負責任務的調度、資源的分配和管理,統領整個計算機硬件;應用程序則是具有某種功能的程序,程序是運行於操作系統之上的。

  進程是一個具有一定獨立功能的程序在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。進程是一種抽象的概念,從來沒有統一的標準定義。

進程一般由程序、數據集合和進程控制塊三部分組成。

  • 程序用於描述進程要完成的功能,是控制進程執行的指令集;
  • 數據集合是程序在執行時所需要的數據和工作區;
  • 程序控制塊(Program Control Block,簡稱PCB),包含進程的描述信息和控制信息,是進程存在的唯一標誌。

進程具有的特徵:

  • 動態性:進程是程序的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的;
  • 併發性:任何進程都可以同其他進程一起併發執行;
  • 獨立性:進程是系統進行資源分配和調度的一個獨立單位;
  • 結構性:進程由程序、數據和進程控制塊三部分組成。

線程

  在早期的操作系統中並沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。任務調度採用的是時間片輪轉的搶佔式調度方式,而進程是任務調度的最小單位,每個進程有各自獨立的一塊內存,使得各個進程之間內存地址相互隔離。

  後來,隨着計算機的發展,對CPU的要求越來越高,進程之間的切換開銷較大,已經無法滿足越來越複雜的程序的要求了。於是就發明了線程。

  線程是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間)。一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成。而進程由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。
(讀到這裏可能有的讀者迷糊,感覺這和Java的內存空間模型不太一樣,但如果你深入的讀過深入理解Java虛擬機這本書的話你就會恍然大悟)

image

如上圖,在任務管理器的進程一欄裏,有道詞典和有道雲筆記就是進程,而在進程下又有着多個執行不同任務的線程。

任務調度

  線程是什麼?要理解這個概念,需要先了解一下操作系統的一些相關概念。大部分操作系統(如Windows、Linux)的任務調度是採用時間片輪轉的搶佔式調度方式。

  在一個進程中,當一個線程任務執行幾毫秒後,會由操作系統的內核(負責管理各個任務)進行調度,通過硬件的計數器中斷處理器,讓該線程強制暫停並將該線程的寄存器放入內存中,通過查看線程列表決定接下來執行哪一個線程,並從內存中恢復該線程的寄存器,最後恢復該線程的執行,從而去執行下一個任務。
上述過程中,任務執行的那一小段時間叫做時間片,任務正在執行時的狀態叫運行狀態,被暫停的線程任務狀態叫做就緒狀態,意爲等待下一個屬於它的時間片的到來。

  這種方式保證了每個線程輪流執行,由於CPU的執行效率非常高,時間片非常短,在各個任務之間快速地切換,給人的感覺就是多個任務在“同時進行”,這也就是我們所說的併發(別覺得併發有多高深,它的實現很複雜,但它的概念很簡單,就是一句話:多個任務同時執行)。多任務運行過程的示意圖如下:

image

圖1:操作系統中的任務調度

進程與線程的區別

  前面講了進程與線程,但可能你還覺得迷糊,感覺他們很類似。的確,進程與線程有着千絲萬縷的關係,下面就讓我們一起來理一理:

  1. 線程是程序執行的最小單位,而進程是操作系統分配資源的最小單位;
  2. 一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線;
  3. 進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號),某進程內的線程在其它進程不可見;
  4. 調度和切換:線程上下文切換比進程上下文切換要快得多。
      線程與進程關係的示意圖:

image

圖2:進程與線程的資源共享關係


image

圖3:單線程與多線程的關係



  總之,線程和進程都是一種抽象的概念,線程是一種比進程更小的抽象,線程和進程都可用於實現併發。
在早期的操作系統中並沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。它相當於一個進程裏只有一個線程,進程本身就是線程。所以線程有時被稱爲輕量級進程(Lightweight Process,LWP)。

image

圖4:早期的操作系統只有進程,沒有線程


後來,隨着計算機的發展,對多個任務之間上下文切換的效率要求越來越高,就抽象出一個更小的概念——線程,一般一個進程會有多個(也可是一個)線程。
  
image

圖5:線程的出現,使得一個進程可以有多個線程

多線程與多核

  上面提到的時間片輪轉的調度方式說一個任務執行一小段時間後強制暫停去執行下一個任務,每個任務輪流執行。很多操作系統的書都說“同一時間點只有一個任務在執行”。那有人可能就要問雙核處理器呢?難道兩個核不是同時運行嗎?

  其實“同一時間點只有一個任務在執行”這句話是不準確的,至少它是不全面的。那多核處理器的情況下,線程是怎樣執行呢?這就需要了解內核線程。

  多核(心)處理器是指在一個處理器上集成多個運算核心從而提高計算能力,也就是有多個真正並行計算的處理核心,每一個處理核心對應一個內核線程。
內核線程(Kernel Thread,KLT)就是直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操作調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。一般一個處理核心對應一個內核線程,比如單核處理器對應一個內核線程,雙核處理器對應兩個內核線程,四核處理器對應四個內核線程。

  現在的電腦一般是雙核四線程、四核八線程,是採用超線程技術將一個物理處理核心模擬成兩個邏輯處理核心,對應兩個內核線程,所以在操作系統中看到的CPU數量是實際物理CPU數量的兩倍,如你的電腦是雙核四線程,打開“任務管理器\性能”可以看到4個CPU的監視器,四核八線程可以看到8個CPU的監視器。

image

圖6:雙核四線程在Windows8下查看的結果

  超線程技術就是利用特殊的硬件指令,把一個物理芯片模擬成兩個邏輯處理核心,讓單個處理器都能使用線程級並行計算,進而兼容多線程操作系統和軟件,減少了CPU的閒置時間,提高的CPU的運行效率。這種超線程技術(如雙核四線程)由處理器硬件的決定,同時也需要操作系統的支持才能在計算機中表現出來。

  程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Lightweight Process,LWP),輕量級進程就是我們通常意義上所講的線程,也被叫做用戶線程。由於每個輕量級進程都由一個內核線程支持,因此只有先支持內核線程,纔能有輕量級進程。用戶線程與內核線程的對應關係有三種模型:一對一模型、多對一模型、多對多模型,在這以4個內核線程、3個用戶線程爲例對三種模型進行說明。

一對一模型

  對於一對一模型來說,一個用戶線程就唯一地對應一個內核線程(反過來不一定成立,一個內核線程不一定有對應的用戶線程)。這樣,如果CPU沒有采用超線程技術(如四核四線程的計算機),一個用戶線程就唯一地映射到一個物理CPU的內核線程,線程之間的併發是真正的併發。一對一模型使用戶線程具有與內核線程一樣的優點,一個線程因某種原因阻塞時其他線程的執行不受影響;此處,一對一模型也可以讓多線程程序在多處理器的系統上有更好的表現。

但一對一模型也有兩個缺點:

  1. 許多操作系統限制了內核線程的數量,因此一對一模型會使用戶線程的數量受到限制;
  2. 許多操作系統內核線程調度時,上下文切換的開銷較大,導致用戶線程的執行效率下降。

image

圖7:一對一模型

多對一模型

  多對一模型將多個用戶線程映射到一個內核線程上,線程之間的切換由用戶態的代碼來進行,系統內核感受不到線程的實現方式。用戶線程的建立、同步、銷燬等都在用戶態中完成,不需要內核的介入。因此相對一對一模型,多對一模型的線程上下文切換速度要快許多;此外,多對一模型對用戶線程的數量幾乎無限制。

但多對一模型也有兩個缺點:

  1. 如果其中一個用戶線程阻塞,那麼其它所有線程都將無法執行,因爲此時內核線程也隨之阻塞了;
  2. 在多處理器系統上,處理器數量的增加對多對一模型的線程性能不會有明顯的增加,因爲所有的用戶線程都映射到一個處理器上了。


image

圖8:多對一模型

多對多模型

  多對多模型結合了一對一模型和多對一模型的優點,將多個用戶線程映射到多個內核線程上。由線程庫負責在可用的可調度實體上調度用戶線程,這使得線程的上下文切換非常快,因爲它避免了系統調用。但是增加了複雜性和優先級倒置的可能性,以及在用戶態調度程序和內核調度程序之間沒有廣泛(且高昂)協調的次優調度。

多對多模型的優點有:

  1. 一個用戶線程的阻塞不會導致所有線程的阻塞,因爲此時還有別的內核線程被調度來執行;
  2. 多對多模型對用戶線程的數量沒有限制;
  3. 在多處理器的操作系統中,多對多模型的線程也能得到一定的性能提升,但提升的幅度不如一對一模型的高。

image

圖9:多對多模型


在現在流行的操作系統中,大都採用多對多的模型。

查看進程與線程

  一個應用程序可能是多線程的,也可能是多進程的,如何查看呢?在Windows下我們只須打開任務管理器就能查看一個應用程序的進程和線程數。按“Ctrl+Alt+Del”或右鍵快捷工具欄打開任務管理器。

  查看進程數和線程數:

image

圖10:查看線程數和進程數


  在“進程”選項卡下,我們可以看到一個應用程序包含的線程數。如果一個應用程序有多個進程,我們能看到每一個進程,如在上圖中,Google的Chrome瀏覽器就有多個進程。同時,如果打開了一個應用程序的多個實例也會有多個進程,如上圖中我打開了兩個cmd窗口,就有兩個cmd進程。如果看不到線程數這一列,可以再點擊“查看\選擇列”菜單,增加監聽的列。
  查看CPU和內存的使用率:
  在性能選項卡中,我們可以查看CPU和內存的使用率,根據CPU使用記錄的監視器的個數還能看出邏輯處理核心的個數,如我的雙核四線程的計算機就有四個監視器。

image

圖11:查看CPU和內存的使用率


線程的生命週期

  當線程的數量小於處理器的數量時,線程的併發是真正的併發,不同的線程運行在不同的處理器上。但當線程的數量大於處理器的數量時,線程的併發會受到一些阻礙,此時並不是真正的併發,因爲此時至少有一個處理器會運行多個線程。

  在單個處理器運行多個線程時,併發是一種模擬出來的狀態。操作系統採用時間片輪轉的方式輪流執行每一個線程。現在,幾乎所有的現代操作系統採用的都是時間片輪轉的搶佔式調度方式,如我們熟悉的Unix、Linux、Windows及macOS等流行的操作系統。

  我們知道線程是程序執行的最小單位,也是任務執行的最小單位。在早期只有進程的操作系統中,進程有五種狀態,創建、就緒、運行、阻塞(等待)、退出。早期的進程相當於現在的只有單個線程的進程,那麼現在的多線程也有五種狀態,現在的多線程的生命週期與早期進程的生命週期類似。

image

圖12:早期進程的生命週期


  進程在運行過程有三種狀態:就緒、運行、阻塞,創建和退出狀態描述的是進程的創建過程和退出過程。

  • 創建:進程正在創建,還不能運行。操作系統在創建進程時要進行的工作包括分配和建立進程控制塊表項、建立資源表格並分配資源、加載程序並建立地址空間;
  • 就緒:時間片已用完,此線程被強制暫停,等待下一個屬於它的時間片到來;
  • 運行:此線程正在執行,正在佔用時間片;
  • 阻塞:也叫等待狀態,等待某一事件(如IO或另一個線程)執行完;
  • 退出:進程已結束,所以也稱結束狀態,釋放操作系統分配的資源。

image

圖13:線程的生命週期


  • 創建:一個新的線程被創建,等待該線程被調用執行;
  • 就緒:時間片已用完,此線程被強制暫停,等待下一個屬於它的時間片到來;
  • 運行:此線程正在執行,正在佔用時間片;
  • 阻塞:也叫等待狀態,等待某一事件(如IO或另一個線程)執行完;
  • 退出:一個線程完成任務或者其他終止條件發生,該線程終止進入退出狀態,退出狀態釋放該線程所分配的資源。

協程

協程,英文Coroutines,是一種基於線程之上,但又比線程更加輕量級的存在,這種由程序員自己寫程序來管理的輕量級線程叫做『用戶空間線程』,具有對內核來說不可見的特性。

因爲是自主開闢的異步任務,所以很多人也更喜歡叫它們纖程(Fiber),或者綠色線程(GreenThread)。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。

image

協程的目的

在傳統的J2EE系統中都是基於每個請求佔用一個線程去完成完整的業務邏輯(包括事務)。所以系統的吞吐能力取決於每個線程的操作耗時。如果遇到很耗時的I/O行爲,則整個系統的吞吐立刻下降,因爲這個時候線程一直處於阻塞狀態,如果線程很多的時候,會存在很多線程處於空閒狀態(等待該線程執行完才能執行),造成了資源應用不徹底。

最常見的例子就是JDBC(它是同步阻塞的),這也是爲什麼很多人都說數據庫是瓶頸的原因。這裏的耗時其實是讓CPU一直在等待I/O返回,說白了線程根本沒有利用CPU去做運算,而是處於空轉狀態。而另外過多的線程,也會帶來更多的ContextSwitch開銷。

對於上述問題,現階段行業裏的比較流行的解決方案之一就是單線程加上異步回調。其代表派是node.js以及Java裏的新秀Vert.x。

而協程的目的就是當出現長時間的I/O操作時,通過讓出目前的協程調度,執行下一個任務的方式,來消除ContextSwitch上的開銷。

協程的特點

  1. 線程的切換由操作系統負責調度,協程由用戶自己進行調度,因此減少了上下文切換,提高了效率。
  2. 線程的默認Stack大小是1M,而協程更輕量,接近1K。因此可以在相同的內存中開啓更多的協程。
  3. 由於在同一個線程上,因此可以避免競爭關係而使用鎖。
  4. 適用於被阻塞的,且需要大量併發的場景。但不適用於大量計算的多線程,遇到此種情況,更好實用線程去解決。

協程的原理

當出現IO阻塞的時候,由協程的調度器進行調度,通過將數據流立刻yield掉(主動讓出),並且記錄當前棧上的數據,阻塞完後立刻再通過線程恢復棧,並把阻塞的結果放到這個線程上去跑,這樣看上去好像跟寫同步代碼沒有任何差別,這整個流程可以稱爲coroutine,而跑在由coroutine負責調度的線程稱爲Fiber。比如Golang裏的 go關鍵字其實就是負責開啓一個Fiber,讓func邏輯跑在上面。

由於協程的暫停完全由程序控制,發生在用戶態上;而線程的阻塞狀態是由操作系統內核來進行切換,發生在內核態上。
因此,協程的開銷遠遠小於線程的開銷,也就沒有了ContextSwitch上的開銷。

協程和線程的比較

比較項 線程 協程
佔用資源 初始單位爲1MB,固定不可變 初始一般爲 2KB,可隨需要而增大
調度所屬 由 OS 的內核完成 由用戶完成
切換開銷 涉及模式切換(從用戶態切換到內核態)、16個寄存器、PC、SP...等寄存器的刷新等 只有三個寄存器的值修改 - PC / SP / DX.
性能問題 資源佔用太高,頻繁創建銷燬會帶來嚴重的性能問題 資源佔用小,不會帶來嚴重的性能問題
數據同步 需要用鎖等機制確保數據的一直性和可見性 不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章