天天看點

一文搞懂程序、線程、協程

前言

作業系統的主要目标是執行使用者程式,但也需要顧及核心之外的各種系統任務。

系統由一組程序組成: 作業系統程序執行系統代碼,使用者程序執行使用者代碼。

問題:為什麼需要程序?

早期的計算機系統隻允許一次執行一個程式,這種程式對系統有完全的控制,能通路所有的系統資源。

現代計算機系統允許将多個程式調入記憶體并發執行,這一要求對各種程式提供更嚴格的控制和更好的劃分。

這些需求産生了程序的概念,即執行中的程式,程序是現代分時系統的工作單元。

  • 多道程式設計的目的是:無論何時都有程序在運作,進而使​

    ​CPU​

    ​ 使用率達到最大化。
  • 分時系統的目的是:在程序之間快速切換​

    ​CPU​

    ​ 以便使用者在程式運作時能與其進行互動。

本文将圍繞這兩個問題和一個案例展開:

一文搞懂程式、線程、協程

程序

程式與程序差別: 程序是活動實體

  • 程式隻是被動實體,如存儲在磁盤上包含一系列指令的檔案内容(指:可執行檔案)。
  • 當一個可執行檔案被裝入記憶體時,一個程式才能成為一個要執行的指令和相關資源集合。

是以,程序可看做是正在執行的程式

程序需要一定的資源(如 ​

​CPU​

​、時間、記憶體、檔案和 ​

​I/O​

​ 裝置)來完成其任務。 這些資源在建立程序或者執行程序時被配置設定。

程序的組成有:​

​PCB​

​​、程式段、資料段

  • ​PCB​

    ​​(程序控制塊,​

    ​process control block​

    ​):儲存程序運作期間相關的資料,是程序存在的唯一标志。
  • 程式段:能被程序排程程式排程到​

    ​CPU​

    ​ 運作的程式的代碼段。
  • 資料段:存儲程式運作期間的相關資料,可以是原始資料也可以是相關結果。
一文搞懂程式、線程、協程

程序在執行時會改變狀态,程序的狀态有 5 種:

  • 建立:程序正在被建立。
  • 運作:指令正在被執行。
  • 等待:阻塞,程序等待某個事件的發生(如​

    ​I/O​

    ​ 完成或收到信号)。
  • 就緒:程序等待配置設定處理器。
  • 終止:程序完成執行。
一文搞懂程式、線程、協程

​Tips​

​ :将 ​

​CPU​

​ 切換到另一個程序需要儲存目前程序的狀态并恢複另一個程序的狀态,這一任務稱為上下文切換(​

​context switch​

​) 。

在 ​

​Linux​

​​ 中可通過 ​

​top​

​ 和 ​​

​ps​

​​ 工具檢視程序狀态: ​

​S​

​ 清單示程序的狀态,有 ​

​R​

​、​

​D​

​、​

​Z​

​、​

​S​

​、​

​I​

​ 和 ​

​T​

​ 、​

​X​

$ top
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
28961 root      20   0   43816   3148   4040 R   3.2  0.0   0:00.01 top
  620 root      20   0   37280  33676    908 D   0.3  0.4   0:00.01 app
    1 root      20   0  160072   9416   6752 S   0.0  0.1   0:37.64 systemd
 1896 root      20   0       0      0      0 Z   0.0  0.0   0:00.00 devapp
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.10 kthreadd
    4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H
複制代碼      
  • ​R​

    ​ :Running,表示程序在​

    ​CPU​

    ​ 的就緒隊列中,正在運作或者正在等待運作。
  • ​D​

    ​:​

    ​Disk Sleep​

    ​,不可中斷狀态睡眠,表示程序正在跟硬體互動,并且互動過程不允許被其他程序或中斷打斷。
  • ​Z​

    ​:​

    ​Zombie​

    ​,表示僵屍程序,實際上程序已經結束了,但其父程序還沒有回收它的資源。
  • ​S​

    ​:​

    ​Interruptible Sleep​

    ​,可中斷狀态睡眠,表示程序因為等待某個事件而被系統挂起。
  • ​I​

    ​:​

    ​Idle​

    ​,空閑狀态,用在不可中斷睡眠的核心線程上。可能實際上沒有任何負載。
  • ​T​

    ​:​

    ​Stopped​

    ​,表示程序處于暫停或者跟蹤狀态。
  • ​X​

    ​:​

    ​Dead​

    ​,表示程序已經消亡,不會在​

    ​top​

    ​ 和​

    ​ps​

    ​ 指令中看到。
程序模型:單程序、多程序

伺服器高性能的關鍵之一在于:并發模型,其有兩個關鍵設計點:

  • 如何管理連接配接?
  • 如何處理請求?

傳統 ​

​UNIX​

​​ 網絡伺服器采用模型有:

  • ​PPC​

    ​(​

    ​Process Per Connection​

    ​):指每次有新的連接配接就建立一個程序去專門處理這個連接配接。
  • ​Prefock​

    ​ :提前建立程序,預先建立好程序才開始接受使用者的請求(省去​

    ​fork​

    ​ 程序的操作)。
一文搞懂程式、線程、協程

這種模式的弊端有三:

  1. ​fork​

    ​​ 代價高:建立一個程序,需要配置設定很多核心資源,需要将記憶體映像從父程序複制到子程序。
  2. 父子程序通信複雜:父程序 “fork”子程序時,檔案描述符可以通過記憶體映像複制從父程序傳到子程序,但“fork”完成後,父子程序通信就比較麻煩了,需要采用 IPC(Interprocess Communication)之類的程序通信方案。
  3. 支援的并發連接配接數量有限:程序上下文切換消耗大、程序建立占資源大。一般情況下,​

    ​PPC​

    ​ 方案能處理的并發連接配接數量最大也就幾百。

線程

為什麼需要線程? 為了更好地使多道程式并發執行。

如果新程序與現有程序執行同樣的任務,那麼為什麼需要這些開銷呢?

如果一個具有多個線程的程序能達到同樣的目的,那麼将更為有效。

  • 線程是 ​

    ​CPU​

    ​​ 使用的基本單元,由線程​

    ​ID​

    ​、程式計數器、寄存器集合和棧組成。
  • 程序由一個或多個線程組成:​

    ​Linux​

    ​ 中建立一個程序自然會建立一個線程,也就是主線程。
  • 排程切換:線程上下文切換比程序上下文切換快。
  • 程序建立很耗時間和資源:
  • 建立程序:需要為程序劃分出一塊完整的記憶體空間,有大量的初始化操作,比如要把記憶體分段(堆棧、正文區等)。
  • 建立線程:隻需要确定 PC 指針和寄存器的值,并且給線程配置設定一個棧用于執行程式,同一個程序的多個線程間可以複用堆棧。
一文搞懂程式、線程、協程

有兩種不同方法來提供線程支援:

  • 使用者層的使用者線程:适合于​

    ​IO​

    ​ 密集型任務,受核心支援,而無須核心管理。
  • 核心層的核心線程:計算密集型任務,由作業系統直接支援和管理。

使用者線程與核心線程之間對應關系有三種: 多對一模型、一對一模型、多對多模型

一文搞懂程式、線程、協程
  • 多對一模型缺點:任一時刻隻有一個線程能通路核心,多個線程不能并行運作在多處理器上。
  • 一對一模型缺點:每建立一個使用者線程就需要建立相應的核心線程。限制了系統所支援的線程數量。
  • 多對多模型:沒有以上兩者的缺點。開發人員可建立任意多的使用者線程,并且相應核心線程在多處理器系統上并發執行。
多線程、​

​Reactor​

​、​

​Proactor​

常見伺服器高性能的多線程模式:

  • ​TPC​

    ​:每次有新連接配接就建立新線程。
  • ​Prethread​

    ​:提前建立好線程,例如線程池,每次有新連接配接就從線程池裡拿取。
一文搞懂程式、線程、協程

拓展, ​

​I/O​

​​ 模型: 阻塞、非阻塞、同步、異步。

一文搞懂程式、線程、協程

​Reactor​

​​ 是同步非阻塞網絡模型:

  • 有三種典型實作方案:單​

    ​Reactor​

    ​單線程、單​

    ​Reactor​

    ​多線程、主從​

    ​Reactor​

    ​​多線程(常用)。
  • 處理三類事件:連接配接事件、寫事件、讀事件。
  • 三個關鍵角色:​

    ​reactor​

    ​專門監聽和配置設定事件、​

    ​acceptor​

    ​ 處理連接配接事件、​

    ​handler​

    ​ 處理讀寫事件。
一文搞懂程式、線程、協程

舉個栗子:​

​Netty​

​ 主從​

​Reactor​

​​多線程

  • ​BossEventLoopGroup​

    ​ :負責監聽用戶端的​

    ​Accept​

    ​ 事件,當事件觸發時,将事件注冊至​

    ​WorkerEventLoopGroup​

    ​ 中的一個​

    ​NioEventLoop​

    ​ 上。
  • 每建立一個​

    ​Channel​

    ​, 隻選擇一個​

    ​NioEventLoop​

    ​ 與其綁定。
  • ​WorkerEventLoopGroup​

    ​:負責處理​

    ​Read​

    ​ 和​

    ​Write​

    ​ 事件。
一文搞懂程式、線程、協程

最後看 ​

​Proactor​

​: 可以了解為,“來了事件我來處理,處理完了我通知你”

  • “我”:作業系統核心。
  • “事件”:指​

    ​I/O​

    ​事件,有新連接配接、有資料可讀、有資料可寫。
  • “你”:使用者線程。
一文搞懂程式、線程、協程

協程

協程 ​

​coroutines​

​: 本質上是輕量級的線程。因為是自主開辟的異步任務,是以很多人也更喜歡叫它們纖程(​

​Fiber​

​),或者綠色線程(​

​GreenThread​

​)。正如一個程序可以擁有多個線程一樣,一個線程也可以擁有多個協程。

一文搞懂程式、線程、協程

協程特點有:

  • 非阻塞 :具有挂起和恢複的能力。目前協程進行阻塞操作時,
協程不會與特定的線程綁定,它可以在不同的線程之間靈活切換,而這其實也是通過“挂起和恢複”來實作的。
  • 可看作輕量級線程: 占用更少的堆棧空間,并且需要的堆棧大小可以随着程式的運作需要動态增加或者空間回收。
  • 上下文切換發生在使用者态: 切換速度比較快,并且開銷比較小。

同時,協程可以分為兩類:

  1. ​stackfull co-routine​

    ​:切換協程的時候,需要使用棧來儲存資訊,速度稍慢,實作更容易。
  2. ​stackless co-routine​

    ​:切換協程的時候,不需要使用棧來儲存資訊,速度更快,實作更複雜。

拿協程與線程比較:

比較項 線程 協程
占用資源 1MB,固定不變 2KB,可随需要變大
排程所屬 核心 使用者
切換開銷 涉及模式切換(從使用者态切換到核心态)、16個寄存器、PC、SP...等寄存器的重新整理等 隻有三個寄存器的值修改 - PC / SP / DX
資料同步 需要用鎖等機制確定資料的一直性和可見性 不需要多線程的鎖機制,因為隻有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,隻需要判斷狀态就好了,是以執行效率比多線程高很多。

程序 VS 線程 VS 協程:

執行體 位址空間 排程方 時間片排程 主動排程
程序 不同執行體有不同位址空間 作業系統核心 基于時鐘中斷 系統調用
線程 不同執行體共享位址空間 作業系統核心 基于時鐘中斷 系統調用
協程 不同執行體共享位址空間 使用者态 一般不支援 包裝系統調用

​Go​

​ 使用協程

在 ​

​Go​

​ 中,使用 ​

​go​

​ 關鍵字跟上一個函數,就建立一個 ​

​goroutine​

​。

​goroutine​

​ 而不是 ​

​coroutine​

​ 是因為其不是完全協作式的,也存在搶占式排程:
  • 協作式排程:依靠被排程方主動棄權。
  • 搶占式排程:依靠排程器強制将被排程方被動中斷。
一文搞懂程式、線程、協程

先了解 ​

​Go​

​ 線程模型的 3 個概念:核心線程(M)、​

​goroutine​

​​(G)、邏輯處理器(P)

​Go​

​ 的運作時排程 ​

​goroutine​

​ 在邏輯處理器(​

​P​

​)上運作。
  1. 建立的​

    ​goroutine​

    ​ 會被放入​

    ​Go​

    ​ 運作時排程器 ​

    ​schedt​

    ​ 的全局運作隊列中。
  2. ​Go​

    ​ 運作時排程器會把全局隊列中的​

    ​goroutine​

    ​ 配置設定給不同的邏輯處理器(​

    ​P​

    ​)。
  3. 配置設定的​

    ​goroutine​

    ​ 會被放到邏輯處理器(​

    ​P​

    ​)的本地隊列中,當本地隊列中某個​

    ​goroutine​

    ​ 就緒後,待配置設定到時間片後就可以在邏輯處理器上運作了。

繼續閱讀