0 前言
在過去單CPU時代喷斋,單任務(wù)在一個時間點只能執(zhí)行單一程序悍募。之后發(fā)展到多任務(wù)階段庸推,計算機(jī)能在同一時間點并行執(zhí)行多任務(wù)或多進(jìn)程常侦。雖然并不是真正意義上的“同一時間點”浇冰,而是 多個任務(wù)或進(jìn)程共享一個CPU,并交由操作系統(tǒng)來完成多任務(wù)間對CPU的運(yùn)行切換聋亡,以使得每個任務(wù)都有機(jī)會獲得一定的時間片運(yùn)行肘习。
再后來發(fā)展到多線程技術(shù),使得在一個程序內(nèi)部能擁有多個線程并行執(zhí)行坡倔。一個線程的執(zhí)行可以被認(rèn)為是一個CPU在執(zhí)行該程序漂佩。當(dāng)一個程序運(yùn)行在多線程下矩父,就好像有多個CPU在同時執(zhí)行該程序催跪。
多線程比多任務(wù)更加有挑戰(zhàn)。多線程是在同一個程序內(nèi)部并行執(zhí)行心剥,因此會對相同的內(nèi)存空間進(jìn)行并發(fā)讀寫操作征堪。這可能是在單線程程序中從來不會遇到的問題瘩缆。其中的一些錯誤也未必會在單CPU機(jī)器上出現(xiàn),因為兩個線程從來不會得到真正的并行執(zhí)行请契。然而咳榜,更現(xiàn)代的計算機(jī)伴隨著多核CPU的出現(xiàn)夏醉,也就意味著 不同的線程能被不同的CPU核得到真正意義的并行執(zhí)行爽锥。
所以,在多線程畔柔、多任務(wù)情況下氯夷,線程上下文切換是必須的,然而對于CPU架構(gòu)設(shè)計中的概念靶擦,應(yīng)先熟悉了解腮考,這樣會有助于理解線程上下文切換原理。
1 多核玄捕、多CPU踩蔚、超線程、多線程
1.1 為什么要多核
先要說的是多核枚粘、多CPU馅闽、超線程,這三個其實都是CPU架構(gòu)設(shè)計的概念馍迄,一個現(xiàn)代CPU除了處理器核心之外還包括寄存器福也、L1L2緩存這些存儲設(shè)備、浮點運(yùn)算單元攀圈、整數(shù)運(yùn)算單元等一些輔助運(yùn)算設(shè)備以及內(nèi)部總線等暴凑。一個多核的CPU也就是一個CPU上有多個處理器核心,這樣有什么好處呢赘来?比如說現(xiàn)在我們要在一臺計算機(jī)上跑一個多線程的程序现喳,因為是一個進(jìn)程里的線程凯傲,所以需要一些共享一些存儲變量,如果這臺計算機(jī)都是單核單線程CPU的話拿穴,就意味著這個程序的不同線程需要經(jīng)常在CPU之間的外部總線上通信泣洞,同時還要處理不同CPU之間不同緩存導(dǎo)致數(shù)據(jù)不一致的問題,所以在這種場景下多核單CPU的架構(gòu)就能發(fā)揮很大的優(yōu)勢默色,通信都在內(nèi)部總線球凰,共用同一個緩存。
1.2 為什么要多CPU
前面提了多核的好處腿宰,那為什么要多CPU呢呕诉?這個其實很容易想到,如果要運(yùn)行多個程序(進(jìn)程)的話吃度,假如只有一個CPU的話甩挫,就意味著要經(jīng)常進(jìn)行進(jìn)程上下文切換,因為單CPU即便是多核的椿每,也只是多個處理器核心伊者,其他設(shè)備都是共用的,所以 多個進(jìn)程就必然要經(jīng)常進(jìn)行進(jìn)程上下文切換间护,這個代價是很高的亦渗。
1.3 為什么要超線程
超線程這個概念是Intel提出的,簡單來說是在一個CPU上真正的并發(fā)兩個線程汁尺,聽起來似乎不太可能法精,因為CPU都是分時的啊,其實這里也是分時痴突,因為前面也提到一個CPU除了處理器核心還有其他設(shè)備搂蜓,一段代碼執(zhí)行過程也不光是只有處理器核心工作,如果兩個線程A和B辽装,A正在使用處理器核心帮碰,B正在使用緩存或者其他設(shè)備,那AB兩個線程就可以并發(fā)執(zhí)行拾积,但是如果AB都在訪問同一個設(shè)備殉挽,那就只能等前一個線程執(zhí)行完后一個線程才能執(zhí)行。實現(xiàn)這種并發(fā)的原理是 在CPU里加了一個協(xié)調(diào)輔助核心殷勘,根據(jù)Intel提供的數(shù)據(jù)此再,這樣一個設(shè)備會使得設(shè)備面積增大5%,但是性能提高15%~30%玲销。
1.4 為什么要多線程
這個問題也許是面試中問的最多的一個經(jīng)典問題了输拇,一個進(jìn)程里多線程之間可以共享變量,線程間通信開銷也較小贤斜,可以更好的利用多核CPU的性能策吠,多核CPU上跑多線程程序往往會比單線程更快逛裤,有的時候甚至在單核CPU上多線程程序也會有更好的性能,因為雖然多線程會有上下文切換和線程創(chuàng)建銷毀開銷猴抹,但是單線程程序會被IO阻塞無法充分利用CPU資源带族,加上線程的上下文開銷較低以及線程池的大量應(yīng)用,多線程在很多場景下都會有更高的效率蟀给。
1.5 線程與進(jìn)程
進(jìn)程是操作系統(tǒng)的管理單位蝙砌,而線程則是進(jìn)程的管理單位;一個進(jìn)程至少包含一個執(zhí)行線程跋理。不管是在單線程還是多線程中择克,每個線程都有一個程序計數(shù)器(記錄要執(zhí)行的下一條指令),一組寄存器(保存當(dāng)前線程的工作變量)前普,堆棧(記錄執(zhí)行歷史肚邢,其中每一幀保存了一個已經(jīng)調(diào)用但未返回的過程)。雖然線程寄生在進(jìn)程中拭卿,但與他的進(jìn)程是不同的概念骡湖,并且可以分別處理:進(jìn)程是系統(tǒng)分配資源的基本單位,線程是調(diào)度CPU的基本單位峻厚。
一個線程指的是進(jìn)程中一個單一順序的控制流响蕴,一個進(jìn)程中可以并行多個線程,每條線程并行執(zhí)行不同的任務(wù)目木。每個線程共享堆空間换途,擁有自己獨立的棸枚桑空間刽射。
- 線程劃分尺度小于進(jìn)程,線程隸屬于某個進(jìn)程剃执;
- 進(jìn)程是CPU誓禁、內(nèi)存等資源占用的基本單位,線程是不能獨立占有這些資源的肾档;
- 進(jìn)程之間相互獨立摹恰,通信比較困難,而線程之間共享一塊內(nèi)存區(qū)域怒见,通信方便俗慈;
- 進(jìn)程在執(zhí)行過程中,包含:固定的入口遣耍、執(zhí)行順序和出口闺阱,而進(jìn)程的這些過程會被應(yīng)用程序控制;
2 上下文切換
支持多任務(wù)處理是CPU設(shè)計史上最大的跨越之一舵变。在計算機(jī)中酣溃,多任務(wù)處理是指同時運(yùn)行兩個或多個程序瘦穆。從使用者的角度來看,這看起來并不復(fù)雜或者難以實現(xiàn)赊豌,但是它確實是計算機(jī)設(shè)計史上一次大的飛躍扛或。在多任務(wù)處理系統(tǒng)中,CPU需要處理所有程序的操作碘饼,當(dāng)用戶來回切換它們時熙兔,需要記錄這些程序執(zhí)行到哪里。上下文切換就是這樣一個過程艾恼,允許CPU記錄并恢復(fù)各種正在運(yùn)行程序的狀態(tài)黔姜,使它能夠完成切換操作。
多任務(wù)系統(tǒng)往往需要同時執(zhí)行多道作業(yè)蒂萎。作業(yè)數(shù)往往大于機(jī)器的CPU數(shù)秆吵,然而一顆CPU同時只能執(zhí)行一項任務(wù),如何讓用戶感覺這些任務(wù)正在同時進(jìn)行呢? 操作系統(tǒng)的設(shè)計者 巧妙地利用了時間片輪轉(zhuǎn)的方式, CPU給每個任務(wù)都服務(wù)一定的時間五慈,然后把當(dāng)前任務(wù)的狀態(tài)保存下來纳寂,在加載下一任務(wù)的狀態(tài)后,繼續(xù)服務(wù)下一任務(wù)泻拦。任務(wù)的狀態(tài)保存及再加載, 這段過程就叫做上下文切換毙芜。時間片輪轉(zhuǎn)的方式使多個任務(wù)在同一顆CPU上執(zhí)行變成了可能。
2.1 基本概念
上下文切換(有時也稱做進(jìn)程切換或任務(wù)切換)是指CPU從一個進(jìn)程或線程切換到另一個進(jìn)程或線程争拐。
- 進(jìn)程(有時候也稱做任務(wù))是指一個程序運(yùn)行的實例腋粥。
- 在Linux系統(tǒng)中,線程 就是能并行運(yùn)行并且與他們的父進(jìn)程(創(chuàng)建他們的進(jìn)程)共享同一地址空間(一段內(nèi)存區(qū)域)和其他資源的 輕量級的進(jìn)程架曹。
- 上下文 是指某一時間點 CPU 寄存器和程序計數(shù)器的內(nèi)容隘冲。
- 寄存器 是 CPU 內(nèi)部的數(shù)量較少但是速度很快的內(nèi)存(與之對應(yīng)的是 CPU 外部相對較慢的 RAM 主內(nèi)存)。寄存器通過對常用值(通常是運(yùn)算的中間值)的快速訪問來提高計算機(jī)程序運(yùn)行的速度绑雄。
- 程序計數(shù)器是一個專用的寄存器展辞,用于表明指令序列中 CPU 正在執(zhí)行的位置,存的值為正在執(zhí)行的指令的位置或者下一個將要被執(zhí)行的指令的位置万牺,具體依賴于特定的系統(tǒng)罗珍。
上下文切換可以認(rèn)為是內(nèi)核(操作系統(tǒng)的核心)在 CPU 上對于進(jìn)程(包括線程)進(jìn)行以下的活動:
- 掛起一個進(jìn)程,將這個進(jìn)程在 CPU 中的狀態(tài)(上下文)存儲于內(nèi)存中的某處脚粟;
- 恢復(fù)一個進(jìn)程覆旱,在內(nèi)存中檢索下一個進(jìn)程的上下文并將其在 CPU 的寄存器中恢復(fù);
- 跳轉(zhuǎn)到程序計數(shù)器所指向的位置(即跳轉(zhuǎn)到進(jìn)程被中斷時的代碼行)核无,以恢復(fù)該進(jìn)程扣唱。
2.2 切換種類
上下文切換在不同的場合有不同的含義,在下表中列出:
上下文切換種類 | 描述 |
---|---|
線程切換 | 同一進(jìn)程中的兩個線程之間的切換 |
進(jìn)程切換 | 兩個進(jìn)程之間的切換 |
模式切換 | 在給定線程中,用戶模式和內(nèi)核模式的切換 |
地址空間切換 | 將虛擬內(nèi)存切換到物理內(nèi)存 |
2.3 切換步驟
在上下文切換過程中画舌,CPU會停止處理當(dāng)前運(yùn)行的程序堕担,并保存當(dāng)前程序運(yùn)行的具體位置以便之后繼續(xù)運(yùn)行。從這個角度來看曲聂,上下文切換有點像我們同時閱讀幾本書霹购,在來回切換書本的同時我們需要記住每本書當(dāng)前讀到的頁碼。在程序中朋腋,上下文切換過程中的“頁碼”信息是保存在進(jìn)程控制塊(PCB, process control block)中的齐疙。PCB還經(jīng)常被稱作“切換楨”(switchframe)⌒裱剩“頁碼”信息會一直保存到CPU的內(nèi)存中贞奋,直到他們被再次使用。
PCB通常是系統(tǒng)內(nèi)存占用區(qū)中的一個連續(xù)存區(qū)穷绵,它存放著操作系統(tǒng)用于描述進(jìn)程情況及控制進(jìn)程運(yùn)行所需的全部信息轿塔,它使一個在多道程序環(huán)境下不能獨立運(yùn)行的程序成為一個能獨立運(yùn)行的基本單位或一個能與其他進(jìn)程并發(fā)執(zhí)行的進(jìn)程。
- 保存進(jìn)程A的狀態(tài)(寄存器和操作系統(tǒng)數(shù)據(jù))仲墨;
- 更新PCB中的信息勾缭,對進(jìn)程A的“運(yùn)行態(tài)”做出相應(yīng)更改;
- 將進(jìn)程A的PCB放入相關(guān)狀態(tài)的隊列目养;
- 將進(jìn)程B的PCB信息改為“運(yùn)行態(tài)”俩由,并執(zhí)行進(jìn)程B;
- B執(zhí)行完后癌蚁,從隊列中取出進(jìn)程A的PCB幻梯,恢復(fù)進(jìn)程A被切換時的上下文,繼續(xù)執(zhí)行A努释;
線程切換和進(jìn)程切換的步驟也不同碘梢。進(jìn)程的上下文切換分為兩步:
- 切換頁目錄以使用新的地址空間;
- 切換內(nèi)核棧和硬件上下文洽洁;
對于Linux來說痘系,線程和進(jìn)程的最大區(qū)別就在于地址空間菲嘴。對于線程切換饿自,第1步是不需要做的,第2是進(jìn)程和線程切換都要做的龄坪。所以明顯是進(jìn)程切換代價大昭雌。線程上下文切換和進(jìn)程上下文切換一個最主要的區(qū)別是 線程的切換虛擬內(nèi)存空間依然是相同的,但是進(jìn)程切換是不同的健田。這兩種上下文切換的處理都是 通過操作系統(tǒng)內(nèi)核來完成的烛卧。內(nèi)核的這種切換過程伴隨的 最顯著的性能損耗是將寄存器中的內(nèi)容切換出。
對于一個正在執(zhí)行的進(jìn)程包括 程序計數(shù)器、寄存器总放、變量的當(dāng)前值等 呈宇,而這些數(shù)據(jù)都是 保存在CPU的寄存器中的,且這些寄存器只能是正在使用CPU的進(jìn)程才能享用局雄,在進(jìn)程切換時甥啄,首先得保存上一個進(jìn)程的這些數(shù)據(jù)(便于下次獲得CPU的使用權(quán)時從上次的中斷處開始繼續(xù)順序執(zhí)行,而不是返回到進(jìn)程開始炬搭,否則每次進(jìn)程重新獲得CPU時所處理的任務(wù)都是上一次的重復(fù)蜈漓,可能永遠(yuǎn)也到不了進(jìn)程的結(jié)束出,因為一個進(jìn)程幾乎不可能執(zhí)行完所有任務(wù)后才釋放CPU)宫盔,然后將本次獲得CPU的進(jìn)程的這些數(shù)據(jù)裝入CPU的寄存器從上次斷點處繼續(xù)執(zhí)行剩下的任務(wù)融虽。
操作系統(tǒng)為了便于管理系統(tǒng)內(nèi)部進(jìn)程,為每個進(jìn)程創(chuàng)建了一張進(jìn)程表項:
2.4 切換查看
在Linux系統(tǒng)下可以使用vmstat命令來查看上下文切換的次數(shù)灼芭,下面是利用vmstat查看上下文切換次數(shù)的示例:
vmstat 1指每秒統(tǒng)計一次, 其中cs列就是指上下文切換的數(shù)目. 一般情況下, 空閑系統(tǒng)的上下文切換每秒大概在1500以下.
3 切換原因
引起線程上下文切換的原因有额,主要存在三種情況如下:
- 中斷處理:在中斷處理中,其他程序”打斷”了當(dāng)前正在運(yùn)行的程序彼绷。當(dāng)CPU接收到中斷請求時谆吴,會在正在運(yùn)行的程序和發(fā)起中斷請求的程序之間進(jìn)行一次上下文切換。中斷分為硬件中斷和軟件中斷苛预,軟件中斷包括因為IO阻塞句狼、未搶到資源或者用戶代碼等原因,線程被掛起热某。
- 多任務(wù)處理:在多任務(wù)處理中腻菇,CPU會在不同程序之間來回切換,每個程序都有相應(yīng)的處理時間片昔馋,CPU在兩個時間片的間隔中進(jìn)行上下文切換筹吐。
- 用戶態(tài)切換:對于一些操作系統(tǒng),當(dāng)進(jìn)行用戶態(tài)切換時也會進(jìn)行一次上下文切換秘遏,雖然這不是必須的丘薛。
對于我們經(jīng)常 使用的搶占式操作系統(tǒng) 而言,引起線程上下文切換的原因大概有以下幾種:
- 當(dāng)前執(zhí)行任務(wù)的時間片用完之后邦危,系統(tǒng)CPU正常調(diào)度下一個任務(wù)洋侨;
- 當(dāng)前執(zhí)行任務(wù)碰到IO阻塞,調(diào)度器將此任務(wù)掛起倦蚪,繼續(xù)下一任務(wù)希坚;
- 多個任務(wù)搶占鎖資源,當(dāng)前任務(wù)沒有搶到鎖資源陵且,被調(diào)度器掛起裁僧,繼續(xù)下一任務(wù);
- 用戶代碼掛起當(dāng)前任務(wù),讓出CPU時間聊疲;
- 硬件中斷茬底;
4 切換損耗
上下文切換會帶來 直接和間接 兩種因素影響程序性能的消耗。
- 直接消耗:指的是CPU寄存器需要保存和加載, 系統(tǒng)調(diào)度器的代碼需要執(zhí)行, TLB實例需要重新加載, CPU 的pipeline需要刷掉获洲;
- 間接消耗:指的是多核的cache之間得共享數(shù)據(jù), 間接消耗對于程序的影響要看線程工作區(qū)操作數(shù)據(jù)的大凶;
5 減少切換
既然上下文切換會導(dǎo)致額外的開銷昌妹,因此減少上下文切換次數(shù)便可以提高多線程程序的運(yùn)行效率捶枢。但上下文切換又分為2種:
- 讓步式上下文切換:指執(zhí)行線程主動釋放CPU,與鎖競爭嚴(yán)重程度成正比飞崖,可通過減少鎖競爭來避免烂叔;
- 搶占式上下文切換:指線程因分配的時間片用盡而被迫放棄CPU或者被其他優(yōu)先級更高的線程所搶占,一般由于線程數(shù)大于CPU可用核心數(shù)引起固歪,可通過調(diào)整線程數(shù)蒜鸡,適當(dāng)減少線程數(shù)來避免。
所以牢裳,減少上下文切換的方法有無鎖并發(fā)編程逢防、CAS算法、使用最少線程和使用協(xié)程蒲讯。
- 無鎖并發(fā):多線程競爭時忘朝,會引起上下文切換,所以多線程處理數(shù)據(jù)時判帮,可以用一些辦法來避免使用鎖局嘁,如將數(shù)據(jù)的ID按照Hash取模分段,不同的線程處理不同段的數(shù)據(jù)晦墙;
- CAS算法:Java的Atomic包使用CAS算法來更新數(shù)據(jù)悦昵,而不需要加鎖;
- 最少線程:避免創(chuàng)建不需要的線程晌畅,比如任務(wù)很少但指,但是創(chuàng)建了很多線程來處理,這樣會造成大量線程都處于等待狀態(tài)抗楔;
- 使用協(xié)程:在單線程里實現(xiàn)多任務(wù)的調(diào)度棋凳,并在單線程里維持多個任務(wù)間的切換;
6 線程數(shù)目
合理設(shè)置線程數(shù)目谓谦,關(guān)鍵點是:1. 盡量減少線程切換和管理的開支贫橙;2. 最大化利用CPU;
對于1反粥,要求線程數(shù)盡量少,這樣可以減少線程切換和管理的開支;
對于2才顿,要求盡量多的線程莫湘,以保證CPU資源最大化的利用;
所以 對于任務(wù)耗時短的情況郑气,要求線程盡量少幅垮,如果線程太多,有可能出現(xiàn)線程切換和管理的時間尾组,大于任務(wù)執(zhí)行的時間忙芒,那效率就低了;
對于耗時長的任務(wù)讳侨,要分是CPU任務(wù)呵萨,還是IO等類型的任務(wù)。如果是CPU類型的任務(wù)跨跨,線程數(shù)不宜太多潮峦;但是如果是IO類型的任務(wù),線程多一些更好勇婴,可以更充分利用CPU忱嘹。
高并發(fā),低耗時的情況:建議少線程耕渴,只要滿足并發(fā)即可拘悦,因為上下文切換本來就多,并且高并發(fā)就意味著CPU是處于繁忙狀態(tài)的橱脸, 增加更多地線程也不會讓線程得到執(zhí)行時間片窄做,反而會增加線程切換的開銷;例如并發(fā)100慰技,線程池可能設(shè)置為10就可以椭盏;
低并發(fā),高耗時的情況:建議多線程吻商,保證有空閑線程掏颊,接受新的任務(wù);例如并發(fā)10艾帐,線程池可能就要設(shè)置為20乌叶;
高并發(fā)高耗時:1. 要分析任務(wù)類型;2. 增加排隊柒爸;3. 加大線程數(shù)准浴;