☆啃碎并發(三):Java線程上下文切換

0 前言

在過去單CPU時代,單任務在一個時間點只能執行單一程序。之后發展到多任務階段,計算機能在同一時間點并行執行多任務或多進程。雖然并不是真正意義上的“同一時間點”,而是 多個任務或進程共享一個CPU,并交由操作系統來完成多任務間對CPU的運行切換,以使得每個任務都有機會獲得一定的時間片運行

再后來發展到多線程技術,使得在一個程序內部能擁有多個線程并行執行。一個線程的執行可以被認為是一個CPU在執行該程序。當一個程序運行在多線程下,就好像有多個CPU在同時執行該程序

多線程比多任務更加有挑戰。多線程是在同一個程序內部并行執行,因此會對相同的內存空間進行并發讀寫操作。這可能是在單線程程序中從來不會遇到的問題。其中的一些錯誤也未必會在單CPU機器上出現,因為兩個線程從來不會得到真正的并行執行。然而,更現代的計算機伴隨著多核CPU的出現,也就意味著 不同的線程能被不同的CPU核得到真正意義的并行執行

所以,在多線程、多任務情況下,線程上下文切換是必須的,然而對于CPU架構設計中的概念,應先熟悉了解,這樣會有助于理解線程上下文切換原理。

1 多核、多CPU、超線程、多線程

1.1 為什么要多核

先要說的是多核、多CPU、超線程,這三個其實都是CPU架構設計的概念,一個現代CPU除了處理器核心之外還包括寄存器、L1L2緩存這些存儲設備、浮點運算單元、整數運算單元等一些輔助運算設備以及內部總線等。一個多核的CPU也就是一個CPU上有多個處理器核心,這樣有什么好處呢?比如說現在我們要在一臺計算機上跑一個多線程的程序,因為是一個進程里的線程,所以需要一些共享一些存儲變量,如果這臺計算機都是單核單線程CPU的話,就意味著這個程序的不同線程需要經常在CPU之間的外部總線上通信,同時還要處理不同CPU之間不同緩存導致數據不一致的問題,所以在這種場景下多核單CPU的架構就能發揮很大的優勢,通信都在內部總線,共用同一個緩存

1.2 為什么要多CPU

前面提了多核的好處,那為什么要多CPU呢?這個其實很容易想到,如果要運行多個程序(進程)的話,假如只有一個CPU的話,就意味著要經常進行進程上下文切換,因為單CPU即便是多核的,也只是多個處理器核心,其他設備都是共用的,所以 多個進程就必然要經常進行進程上下文切換,這個代價是很高的

1.3 為什么要超線程

超線程這個概念是Intel提出的,簡單來說是在一個CPU上真正的并發兩個線程,聽起來似乎不太可能,因為CPU都是分時的啊,其實這里也是分時,因為前面也提到一個CPU除了處理器核心還有其他設備,一段代碼執行過程也不光是只有處理器核心工作,如果兩個線程A和B,A正在使用處理器核心,B正在使用緩存或者其他設備,那AB兩個線程就可以并發執行,但是如果AB都在訪問同一個設備,那就只能等前一個線程執行完后一個線程才能執行。實現這種并發的原理是 在CPU里加了一個協調輔助核心,根據Intel提供的數據,這樣一個設備會使得設備面積增大5%,但是性能提高15%~30%。

1.4 為什么要多線程

這個問題也許是面試中問的最多的一個經典問題了,一個進程里多線程之間可以共享變量,線程間通信開銷也較小,可以更好的利用多核CPU的性能,多核CPU上跑多線程程序往往會比單線程更快,有的時候甚至在單核CPU上多線程程序也會有更好的性能,因為雖然多線程會有上下文切換和線程創建銷毀開銷,但是單線程程序會被IO阻塞無法充分利用CPU資源,加上線程的上下文開銷較低以及線程池的大量應用,多線程在很多場景下都會有更高的效率

1.5 線程與進程

進程是操作系統的管理單位,而線程則是進程的管理單位;一個進程至少包含一個執行線程。不管是在單線程還是多線程中,每個線程都有一個程序計數器(記錄要執行的下一條指令),一組寄存器(保存當前線程的工作變量),堆棧(記錄執行歷史,其中每一幀保存了一個已經調用但未返回的過程)。雖然線程寄生在進程中,但與他的進程是不同的概念,并且可以分別處理:進程是系統分配資源的基本單位,線程是調度CPU的基本單位

一個線程指的是進程中一個單一順序的控制流,一個進程中可以并行多個線程,每條線程并行執行不同的任務。每個線程共享堆空間,擁有自己獨立的棧空間

  1. 線程劃分尺度小于進程,線程隸屬于某個進程;
  2. 進程是CPU、內存等資源占用的基本單位,線程是不能獨立占有這些資源的;
  3. 進程之間相互獨立,通信比較困難,而線程之間共享一塊內存區域,通信方便;
  4. 進程在執行過程中,包含:固定的入口、執行順序和出口而進程的這些過程會被應用程序控制
進程&線程表項

2 上下文切換

支持多任務處理是CPU設計史上最大的跨越之一。在計算機中,多任務處理是指同時運行兩個或多個程序。從使用者的角度來看,這看起來并不復雜或者難以實現,但是它確實是計算機設計史上一次大的飛躍。在多任務處理系統中,CPU需要處理所有程序的操作,當用戶來回切換它們時,需要記錄這些程序執行到哪里。上下文切換就是這樣一個過程,允許CPU記錄并恢復各種正在運行程序的狀態,使它能夠完成切換操作。

多任務系統往往需要同時執行多道作業。作業數往往大于機器的CPU數,然而一顆CPU同時只能執行一項任務,如何讓用戶感覺這些任務正在同時進行呢? 操作系統的設計者 巧妙地利用了時間片輪轉的方式, CPU給每個任務都服務一定的時間,然后把當前任務的狀態保存下來,在加載下一任務的狀態后,繼續服務下一任務任務的狀態保存及再加載, 這段過程就叫做上下文切換。時間片輪轉的方式使多個任務在同一顆CPU上執行變成了可能。

任務的狀態保存及再加載, 這段過程就叫做上下文切換

2.1 基本概念

上下文切換(有時也稱做進程切換或任務切換)是指CPU從一個進程或線程切換到另一個進程或線程。

  1. 進程(有時候也稱做任務)是指一個程序運行的實例。
  2. 在Linux系統中,線程 就是能并行運行并且與他們的父進程(創建他們的進程)共享同一地址空間(一段內存區域)和其他資源的 輕量級的進程
  3. 上下文 是指某一時間點 CPU 寄存器和程序計數器的內容。
  4. 寄存器 是 CPU 內部的數量較少但是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度
  5. 程序計數器是一個專用的寄存器,用于表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴于特定的系統。

上下文切換可以認為是內核(操作系統的核心)在 CPU 上對于進程(包括線程)進行以下的活動:

  1. 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲于內存中的某處;
  2. 恢復一個進程,在內存中檢索下一個進程的上下文并將其在 CPU 的寄存器中恢復;
  3. 跳轉到程序計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程。

2.2 切換種類

上下文切換在不同的場合有不同的含義,在下表中列出:

上下文切換種類 描述
線程切換 同一進程中的兩個線程之間的切換
進程切換 兩個進程之間的切換
模式切換 在給定線程中,用戶模式和內核模式的切換
地址空間切換 將虛擬內存切換到物理內存

2.3 切換步驟

在上下文切換過程中,CPU會停止處理當前運行的程序,并保存當前程序運行的具體位置以便之后繼續運行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。在程序中,上下文切換過程中的“頁碼”信息是保存在進程控制塊(PCB, process control block)中的。PCB還經常被稱作“切換楨”(switchframe)。“頁碼”信息會一直保存到CPU的內存中,直到他們被再次使用

PCB通常是系統內存占用區中的一個連續存區,它存放著操作系統用于描述進程情況及控制進程運行所需的全部信息,它使一個在多道程序環境下不能獨立運行的程序成為一個能獨立運行的基本單位或一個能與其他進程并發執行的進程。

  1. 保存進程A的狀態(寄存器和操作系統數據);
  2. 更新PCB中的信息,對進程A的“運行態”做出相應更改;
  3. 將進程A的PCB放入相關狀態的隊列
  4. 將進程B的PCB信息改為“運行態”,并執行進程B
  5. B執行完后,從隊列中取出進程A的PCB,恢復進程A被切換時的上下文,繼續執行A

線程切換和進程切換的步驟也不同。進程的上下文切換分為兩步:

  1. 切換頁目錄以使用新的地址空間
  2. 切換內核棧和硬件上下文

對于Linux來說,線程和進程的最大區別就在于地址空間。對于線程切換,第1步是不需要做的,第2是進程和線程切換都要做的。所以明顯是進程切換代價大。線程上下文切換和進程上下文切換一個最主要的區別是 線程的切換虛擬內存空間依然是相同的,但是進程切換是不同的。這兩種上下文切換的處理都是 通過操作系統內核來完成的。內核的這種切換過程伴隨的 最顯著的性能損耗是將寄存器中的內容切換出

對于一個正在執行的進程包括 程序計數器、寄存器、變量的當前值等 ,而這些數據都是 保存在CPU的寄存器中的,且這些寄存器只能是正在使用CPU的進程才能享用在進程切換時,首先得保存上一個進程的這些數據(便于下次獲得CPU的使用權時從上次的中斷處開始繼續順序執行,而不是返回到進程開始,否則每次進程重新獲得CPU時所處理的任務都是上一次的重復,可能永遠也到不了進程的結束出,因為一個進程幾乎不可能執行完所有任務后才釋放CPU),然后將本次獲得CPU的進程的這些數據裝入CPU的寄存器從上次斷點處繼續執行剩下的任務

操作系統為了便于管理系統內部進程,為每個進程創建了一張進程表項:

進程表項

2.4 切換查看

在Linux系統下可以使用vmstat命令來查看上下文切換的次數,下面是利用vmstat查看上下文切換次數的示例:


上線文切換查看

vmstat 1指每秒統計一次, 其中cs列就是指上下文切換的數目. 一般情況下, 空閑系統的上下文切換每秒大概在1500以下.

3 切換原因

引起線程上下文切換的原因,主要存在三種情況如下:

  1. 中斷處理:在中斷處理中,其他程序”打斷”了當前正在運行的程序。當CPU接收到中斷請求時,會在正在運行的程序和發起中斷請求的程序之間進行一次上下文切換。中斷分為硬件中斷和軟件中斷,軟件中斷包括因為IO阻塞、未搶到資源或者用戶代碼等原因,線程被掛起。
  2. 多任務處理:在多任務處理中,CPU會在不同程序之間來回切換,每個程序都有相應的處理時間片,CPU在兩個時間片的間隔中進行上下文切換。
  3. 用戶態切換:對于一些操作系統,當進行用戶態切換時也會進行一次上下文切換,雖然這不是必須的。

對于我們經常 使用的搶占式操作系統 而言,引起線程上下文切換的原因大概有以下幾種:

  1. 當前執行任務的時間片用完之后,系統CPU正常調度下一個任務;
  2. 當前執行任務碰到IO阻塞,調度器將此任務掛起,繼續下一任務;
  3. 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
  4. 用戶代碼掛起當前任務,讓出CPU時間;
  5. 硬件中斷;

4 切換損耗

上下文切換會帶來 直接和間接 兩種因素影響程序性能的消耗。

  1. 直接消耗:指的是CPU寄存器需要保存和加載, 系統調度器的代碼需要執行, TLB實例需要重新加載, CPU 的pipeline需要刷掉;
  2. 間接消耗:指的是多核的cache之間得共享數據, 間接消耗對于程序的影響要看線程工作區操作數據的大小;

5 減少切換

既然上下文切換會導致額外的開銷,因此減少上下文切換次數便可以提高多線程程序的運行效率。但上下文切換又分為2種:

  1. 讓步式上下文切換:指執行線程主動釋放CPU,與鎖競爭嚴重程度成正比,可通過減少鎖競爭來避免;
  2. 搶占式上下文切換:指線程因分配的時間片用盡而被迫放棄CPU或者被其他優先級更高的線程所搶占,一般由于線程數大于CPU可用核心數引起,可通過調整線程數,適當減少線程數來避免。

所以,減少上下文切換的方法有無鎖并發編程、CAS算法、使用最少線程和使用協程

  1. 無鎖并發:多線程競爭時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash取模分段,不同的線程處理不同段的數據;
  2. CAS算法:Java的Atomic包使用CAS算法來更新數據,而不需要加鎖;
  3. 最少線程:避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處于等待狀態;
  4. 使用協程:在單線程里實現多任務的調度,并在單線程里維持多個任務間的切換;

6 線程數目

合理設置線程數目,關鍵點是:1. 盡量減少線程切換和管理的開支;2. 最大化利用CPU

對于1,要求線程數盡量少,這樣可以減少線程切換和管理的開支;

對于2,要求盡量多的線程,以保證CPU資源最大化的利用;

所以 對于任務耗時短的情況,要求線程盡量少,如果線程太多,有可能出現線程切換和管理的時間,大于任務執行的時間,那效率就低了;

對于耗時長的任務,要分是CPU任務,還是IO等類型的任務。如果是CPU類型的任務,線程數不宜太多;但是如果是IO類型的任務,線程多一些更好,可以更充分利用CPU。

高并發,低耗時的情況:建議少線程,只要滿足并發即可,因為上下文切換本來就多,并且高并發就意味著CPU是處于繁忙狀態的, 增加更多地線程也不會讓線程得到執行時間片,反而會增加線程切換的開銷;例如并發100,線程池可能設置為10就可以;

低并發,高耗時的情況:建議多線程,保證有空閑線程,接受新的任務;例如并發10,線程池可能就要設置為20;

高并發高耗時:1. 要分析任務類型;2. 增加排隊;3. 加大線程數;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容

  • 理解平均負載( Load Average),并用三個案例展示了不同場景下平均負載升高的分析方法。這其中,多個進程競...
    養碼哥閱讀 1,360評論 0 1
  • Linux 是一個多任務操作系統,它支持遠大于 CPU 數量的任務同時運行。當然,這些任務實際上并不是真的在同時運...
    taj3991閱讀 300評論 0 0
  • Linux 是一個多任務操作系統,它支持遠大于 CPU 數量的任務同時運行。當然,這些任務實際上并不是真的在同時運...
    taj3991閱讀 309評論 0 0
  • 讀經: 《創世記》第6章。 經文: 挪亞的后代記在下面。挪亞是個義人,在當時的世代是個完全人。挪亞與 神同行。(創...
    君自爾出閱讀 2,665評論 0 2
  • 世間因果皆尋常 我睡著了,醒來的時候月亮俏生生的臉在淺藍的天空中笑意盈盈望著我,想起來有人跟我說,若能在這樣的時節...
    薛錦嫻閱讀 325評論 0 0