在過去單CPU時代,單任務在一個時間點只能執行單一程序。以後發展到多任務階段,計算機能在同一時間點並行執行多任務或多進程。雖然並非真正意義上的「同一時間點」,而是 多個任務或進程共享一個CPU,並交由操做系統來完成多任務間對CPU的運行切換,以使得每一個任務都有機會得到必定的時間片運行。面試
再後來發展到多線程技術,使得在一個程序內部能擁有多個線程並行執行。一個線程的執行能夠被認爲是一個CPU在執行該程序。當一個程序運行在多線程下,就好像有多個CPU在同時執行該程序。算法
多線程比多任務更加有挑戰。多線程是在同一個程序內部並行執行,所以會對相同的內存空間進行併發讀寫操做。這多是在單線程程序中歷來不會遇到的問題。其中的一些錯誤也未必會在單CPU機器上出現,由於兩個線程歷來不會獲得真正的並行執行。然而,更現代的計算機伴隨着多核CPU的出現,也就意味着 不一樣的線程能被不一樣的CPU核獲得真正意義的並行執行。編程
因此,在多線程、多任務狀況下,線程上下文切換是必須的,然而對於CPU架構設計中的概念,應先熟悉瞭解,這樣會有助於理解線程上下文切換原理。緩存
先要說的是多核、多CPU、超線程,這三個其實都是CPU架構設計的概念,一個現代CPU除了處理器核心以外還包括寄存器、L1L2緩存這些存儲設備、浮點運算單元、整數運算單元等一些輔助運算設備以及內部總線等。一個多核的CPU也就是一個CPU上有多個處理器核心,這樣有什麼好處呢?好比說如今咱們要在一臺計算機上跑一個多線程的程序,由於是一個進程裏的線程,因此須要一些共享一些存儲變量,若是這臺計算機都是單核單線程CPU的話,就意味着這個程序的不一樣線程須要常常在CPU之間的外部總線上通訊,同時還要處理不一樣CPU之間不一樣緩存致使數據不一致的問題,因此在這種場景下多核單CPU的架構就能發揮很大的優點,通訊都在內部總線,共用同一個緩存。多線程
前面提了多核的好處,那爲何要多CPU呢?這個其實很容易想到,若是要運行多個程序(進程)的話,假如只有一個CPU的話,就意味着要常常進行進程上下文切換,由於單CPU即使是多核的,也只是多個處理器核心,其餘設備都是共用的,因此 多個進程就必然要常常進行進程上下文切換,這個代價是很高的。架構
超線程這個概念是Intel提出的,簡單來講是在一個CPU上真正的併發兩個線程,聽起來彷佛不太可能,由於CPU都是分時的啊,其實這裏也是分時,由於前面也提到一個CPU除了處理器核心還有其餘設備,一段代碼執行過程也不光是隻有處理器核心工做,若是兩個線程A和B,A正在使用處理器核心,B正在使用緩存或者其餘設備,那AB兩個線程就能夠併發執行,可是若是AB都在訪問同一個設備,那就只能等前一個線程執行完後一個線程才能執行。實現這種併發的原理是 在CPU里加了一個協調輔助核心,根據Intel提供的數據,這樣一個設備會使得設備面積增大5%,可是性能提升15%~30%。併發
這個問題也許是面試中問的最多的一個經典問題了,一個進程裏多線程之間能夠共享變量,線程間通訊開銷也較小,能夠更好的利用多核CPU的性能,多核CPU上跑多線程程序每每會比單線程更快,有的時候甚至在單核CPU上多線程程序也會有更好的性能,由於雖然多線程會有上下文切換和線程建立銷燬開銷,可是單線程程序會被IO阻塞沒法充分利用CPU資源,加上線程的上下文開銷較低以及線程池的大量應用,多線程在不少場景下都會有更高的效率。高併發
進程是操做系統的管理單位,而線程則是進程的管理單位;一個進程至少包含一個執行線程。無論是在單線程仍是多線程中,每一個線程都有一個程序計數器(記錄要執行的下一條指令),一組寄存器(保存當前線程的工做變量),堆棧(記錄執行歷史,其中每一幀保存了一個已經調用但未返回的過程)。雖然線程寄生在進程中,但與他的進程是不一樣的概念,而且能夠分別處理:進程是系統分配資源的基本單位,線程是調度CPU的基本單位。性能
一個線程指的是進程中一個單一順序的控制流,一個進程中能夠並行多個線程,每條線程並行執行不一樣的任務。每一個線程共享堆空間,擁有本身獨立的棧空間。操作系統
- 線程劃分尺度小於進程,線程隸屬於某個進程;
- 進程是CPU、內存等資源佔用的基本單位,線程是不能獨立佔有這些資源的;
- 進程之間相互獨立,通訊比較困難,而線程之間共享一塊內存區域,通訊方便;
- 進程在執行過程當中,包含:固定的入口、執行順序和出口,而進程的這些過程會被應用程序控制;
支持多任務處理是CPU設計史上最大的跨越之一。在計算機中,多任務處理是指同時運行兩個或多個程序。從使用者的角度來看,這看起來並不複雜或者難以實現,可是它確實是計算機設計史上一次大的飛躍。在多任務處理系統中,CPU須要處理全部程序的操做,當用戶來回切換它們時,須要記錄這些程序執行到哪裏。上下文切換就是這樣一個過程,容許CPU記錄並恢復各類正在運行程序的狀態,使它可以完成切換操做。
多任務系統每每須要同時執行多道做業。做業數每每大於機器的CPU數,然而一顆CPU同時只能執行一項任務,如何讓用戶感受這些任務正在同時進行呢? 操做系統的設計者 巧妙地利用了時間片輪轉的方式, CPU給每一個任務都服務必定的時間,而後把當前任務的狀態保存下來,在加載下一任務的狀態後,繼續服務下一任務。任務的狀態保存及再加載, 這段過程就叫作上下文切換。時間片輪轉的方式使多個任務在同一顆CPU上執行變成了可能。
上下文切換(有時也稱作進程切換或任務切換)是指CPU從一個進程或線程切換到另外一個進程或線程。
- 進程(有時候也稱作任務)是指一個程序運行的實例。
- 在Linux系統中,線程 就是能並行運行而且與他們的父進程(建立他們的進程)共享同一地址空間(一段內存區域)和其餘資源的 輕量級的進程。
- 上下文 是指某一時間點 CPU 寄存器和程序計數器的內容。
- 寄存器 是 CPU 內部的數量較少可是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器經過對經常使用值(一般是運算的中間值)的快速訪問來提升計算機程序運行的速度。
- 程序計數器是一個專用的寄存器,用於代表指令序列中 CPU 正在執行的位置,存的值爲正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。
上下文切換能夠認爲是內核(操做系統的核心)在 CPU 上對於進程(包括線程)進行如下的活動:
- 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於內存中的某處;
- 恢復一個進程,在內存中檢索下一個進程的上下文並將其在 CPU 的寄存器中恢復;
- 跳轉到程序計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程。
上下文切換在不一樣的場合有不一樣的含義,在下表中列出:
上下文切換種類 | 描述 |
---|---|
線程切換 | 同一進程中的兩個線程之間的切換 |
進程切換 | 兩個進程之間的切換 |
模式切換 | 在給定線程中,用戶模式和內核模式的切換 |
地址空間切換 | 將虛擬內存切換到物理內存 |
在上下文切換過程當中,CPU會中止處理當前運行的程序,並保存當前程序運行的具體位置以便以後繼續運行。從這個角度來看,上下文切換有點像咱們同時閱讀幾本書,在來回切換書本的同時咱們須要記住每本書當前讀到的頁碼。在程序中,上下文切換過程當中的「頁碼」信息是保存在進程控制塊(PCB, process control block)中的。PCB還常常被稱做「切換楨」(switchframe)。「頁碼」信息會一直保存到CPU的內存中,直到他們被再次使用。
PCB一般是系統內存佔用區中的一個連續存區,它存放着操做系統用於描述進程狀況及控制進程運行所需的所有信息,它使一個在多道程序環境下不能獨立運行的程序成爲一個能獨立運行的基本單位或一個能與其餘進程併發執行的進程。
- 保存進程A的狀態(寄存器和操做系統數據);
- 更新PCB中的信息,對進程A的「運行態」作出相應更改;
- 將進程A的PCB放入相關狀態的隊列;
- 將進程B的PCB信息改成「運行態」,並執行進程B;
- B執行完後,從隊列中取出進程A的PCB,恢復進程A被切換時的上下文,繼續執行A;
線程切換和進程切換的步驟也不一樣。進程的上下文切換分爲兩步:
- 切換頁目錄以使用新的地址空間;
- 切換內核棧和硬件上下文;
對於Linux來講,線程和進程的最大區別就在於地址空間。對於線程切換,第1步是不須要作的,第2是進程和線程切換都要作的。因此明顯是進程切換代價大。線程上下文切換和進程上下文切換一個最主要的區別是 線程的切換虛擬內存空間依然是相同的,可是進程切換是不一樣的。這兩種上下文切換的處理都是 經過操做系統內核來完成的。內核的這種切換過程伴隨的 最顯著的性能損耗是將寄存器中的內容切換出。
對於一個正在執行的進程包括 程序計數器、寄存器、變量的當前值等 ,而這些數據都是 保存在CPU的寄存器中的,且這些寄存器只能是正在使用CPU的進程才能享用,在進程切換時,首先得保存上一個進程的這些數據(便於下次得到CPU的使用權時從上次的中斷處開始繼續順序執行,而不是返回到進程開始,不然每次進程從新得到CPU時所處理的任務都是上一次的重複,可能永遠也到不了進程的結束出,由於一個進程幾乎不可能執行完全部任務後才釋放CPU),而後將本次得到CPU的進程的這些數據裝入CPU的寄存器從上次斷點處繼續執行剩下的任務。
操做系統爲了便於管理系統內部進程,爲每一個進程建立了一張進程表項:
在Linux系統下可使用vmstat命令來查看上下文切換的次數,下面是利用vmstat查看上下文切換次數的示例:
vmstat 1指每秒統計一次, 其中cs列就是指上下文切換的數目. 通常狀況下, 空閒系統的上下文切換每秒大概在1500如下.
引發線程上下文切換的緣由,主要存在三種狀況以下:
- 中斷處理:在中斷處理中,其餘程序」打斷」了當前正在運行的程序。當CPU接收到中斷請求時,會在正在運行的程序和發起中斷請求的程序之間進行一次上下文切換。中斷分爲硬件中斷和軟件中斷,軟件中斷包括由於IO阻塞、未搶到資源或者用戶代碼等緣由,線程被掛起。
- 多任務處理:在多任務處理中,CPU會在不一樣程序之間來回切換,每一個程序都有相應的處理時間片,CPU在兩個時間片的間隔中進行上下文切換。
- 用戶態切換:對於一些操做系統,當進行用戶態切換時也會進行一次上下文切換,雖然這不是必須的。
對於咱們常常 使用的搶佔式操做系統 而言,引發線程上下文切換的緣由大概有如下幾種:
- 當前執行任務的時間片用完以後,系統CPU正常調度下一個任務;
- 當前執行任務碰到IO阻塞,調度器將此任務掛起,繼續下一任務;
- 多個任務搶佔鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
- 用戶代碼掛起當前任務,讓出CPU時間;
- 硬件中斷;
上下文切換會帶來 直接和間接 兩種因素影響程序性能的消耗。
- 直接消耗:指的是CPU寄存器須要保存和加載, 系統調度器的代碼須要執行, TLB實例須要從新加載, CPU 的pipeline須要刷掉;
- 間接消耗:指的是多核的cache之間得共享數據, 間接消耗對於程序的影響要看線程工做區操做數據的大小;
既然上下文切換會致使額外的開銷,所以減小上下文切換次數即可以提升多線程程序的運行效率。但上下文切換又分爲2種:
- 讓步式上下文切換:指執行線程主動釋放CPU,與鎖競爭嚴重程度成正比,可經過減小鎖競爭來避免;
- 搶佔式上下文切換:指線程因分配的時間片用盡而被迫放棄CPU或者被其餘優先級更高的線程所搶佔,通常因爲線程數大於CPU可用核心數引發,可經過調整線程數,適當減小線程數來避免。
因此,減小上下文切換的方法有無鎖併發編程、CAS算法、使用最少線程和使用協程。
- 無鎖併發:多線程競爭時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的ID按照Hash取模分段,不一樣的線程處理不一樣段的數據;
- CAS算法:Java的Atomic包使用CAS算法來更新數據,而不須要加鎖;
- 最少線程:避免建立不須要的線程,好比任務不多,可是建立了不少線程來處理,這樣會形成大量線程都處於等待狀態;
- 使用協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換;
合理設置線程數目,關鍵點是:1. 儘可能減小線程切換和管理的開支;2. 最大化利用CPU;
對於1,要求線程數儘可能少,這樣能夠減小線程切換和管理的開支;
對於2,要求儘可能多的線程,以保證CPU資源最大化的利用;
因此 對於任務耗時短的狀況,要求線程儘可能少,若是線程太多,有可能出現線程切換和管理的時間,大於任務執行的時間,那效率就低了;
對於耗時長的任務,要分是CPU任務,仍是IO等類型的任務。若是是CPU類型的任務,線程數不宜太多;可是若是是IO類型的任務,線程多一些更好,能夠更充分利用CPU。
高併發,低耗時的狀況:建議少線程,只要知足併發便可,由於上下文切換原本就多,而且高併發就意味着CPU是處於繁忙狀態的, 增長更多地線程也不會讓線程獲得執行時間片,反而會增長線程切換的開銷;例如併發100,線程池可能設置爲10就能夠;
低併發,高耗時的狀況:建議多線程,保證有空閒線程,接受新的任務;例如併發10,線程池可能就要設置爲20;
高併發高耗時:1. 要分析任務類型;2. 增長排隊;3. 加大線程數;