我們都知道計算機的核心是CPU,,它承擔(dān)了所有的計算任務(wù),;而操作系統(tǒng)是計算機的管理者,,它負責(zé)任務(wù)的調(diào)度、資源的分配和管理,,統(tǒng)領(lǐng)整個計算機硬件,;應(yīng)用程序則是具有某種功能的程序,程序是運行于操作系統(tǒng)之上的,。
進程是一個具有一定獨立功能的程序在一個數(shù)據(jù)集上的一次動態(tài)執(zhí)行的過程,,是操作系統(tǒng)進行資源分配和調(diào)度的一個獨立單位,是應(yīng)用程序運行的載體,。進程是一種抽象的概念,,從來沒有統(tǒng)一的標(biāo)準(zhǔn)定義。
進程一般由程序,、數(shù)據(jù)集合和進程控制塊三部分組成,。
進程具有的特征:
在早期的操作系統(tǒng)中并沒有線程的概念,,進程是能擁有資源和獨立運行的最小單位,,也是程序執(zhí)行的最小單位。任務(wù)調(diào)度采用的是時間片輪轉(zhuǎn)的搶占式調(diào)度方式,,而進程是任務(wù)調(diào)度的最小單位,,每個進程有各自獨立的一塊內(nèi)存,使得各個進程之間內(nèi)存地址相互隔離。
后來,,隨著計算機的發(fā)展,,對CPU的要求越來越高,進程之間的切換開銷較大,,已經(jīng)無法滿足越來越復(fù)雜的程序的要求了,。于是就發(fā)明了線程。
線程是程序執(zhí)行中一個單一的順序控制流程,,是程序執(zhí)行流的最小單元,是處理器調(diào)度和分派的基本單位,。一個進程可以有一個或多個線程,,各個線程之間共享程序的內(nèi)存空間(也就是所在進程的內(nèi)存空間)。一個標(biāo)準(zhǔn)的線程由線程ID,、當(dāng)前指令指針(PC),、寄存器和堆棧組成。而進程由內(nèi)存空間(代碼,、數(shù)據(jù),、進程空間、打開的文件)和一個或多個線程組成,。
(讀到這里可能有的讀者迷糊,,感覺這和Java的內(nèi)存空間模型不太一樣,但如果你深入的讀過深入理解Java虛擬機這本書的話你就會恍然大悟)
如上圖,,在任務(wù)管理器的進程一欄里,,有道詞典和有道云筆記就是進程,而在進程下又有著多個執(zhí)行不同任務(wù)的線程,。
線程是什么,?要理解這個概念,需要先了解一下操作系統(tǒng)的一些相關(guān)概念,。大部分操作系統(tǒng)(如Windows,、Linux)的任務(wù)調(diào)度是采用時間片輪轉(zhuǎn)的搶占式調(diào)度方式。
在一個進程中,,當(dāng)一個線程任務(wù)執(zhí)行幾毫秒后,,會由操作系統(tǒng)的內(nèi)核(負責(zé)管理各個任務(wù))進行調(diào)度,通過硬件的計數(shù)器中斷處理器,,讓該線程強制暫停并將該線程的寄存器放入內(nèi)存中,,通過查看線程列表決定接下來執(zhí)行哪一個線程,并從內(nèi)存中恢復(fù)該線程的寄存器,,最后恢復(fù)該線程的執(zhí)行,,從而去執(zhí)行下一個任務(wù)。
上述過程中,任務(wù)執(zhí)行的那一小段時間叫做時間片,,任務(wù)正在執(zhí)行時的狀態(tài)叫運行狀態(tài),,被暫停的線程任務(wù)狀態(tài)叫做就緒狀態(tài),意為等待下一個屬于它的時間片的到來,。
這種方式保證了每個線程輪流執(zhí)行,,由于CPU的執(zhí)行效率非常高,時間片非常短,,在各個任務(wù)之間快速地切換,,給人的感覺就是多個任務(wù)在“同時進行”,這也就是我們所說的并發(fā)(別覺得并發(fā)有多高深,,它的實現(xiàn)很復(fù)雜,,但它的概念很簡單,就是一句話:多個任務(wù)同時執(zhí)行),。多任務(wù)運行過程的示意圖如下:
前面講了進程與線程,,但可能你還覺得迷糊,感覺他們很類似,。的確,,進程與線程有著千絲萬縷的關(guān)系,下面就讓我們一起來理一理:
后來,,隨著計算機的發(fā)展,,對多個任務(wù)之間上下文切換的效率要求越來越高,就抽象出一個更小的概念——線程,,一般一個進程會有多個(也可是一個)線程,。
上面提到的時間片輪轉(zhuǎn)的調(diào)度方式說一個任務(wù)執(zhí)行一小段時間后強制暫停去執(zhí)行下一個任務(wù),,每個任務(wù)輪流執(zhí)行,。很多操作系統(tǒng)的書都說“同一時間點只有一個任務(wù)在執(zhí)行”。那有人可能就要問雙核處理器呢,?難道兩個核不是同時運行嗎,?
其實“同一時間點只有一個任務(wù)在執(zhí)行”這句話是不準(zhǔn)確的,至少它是不全面的,。那多核處理器的情況下,線程是怎樣執(zhí)行呢,?這就需要了解內(nèi)核線程,。
多核(心)處理器是指在一個處理器上集成多個運算核心從而提高計算能力,也就是有多個真正并行計算的處理核心,,每一個處理核心對應(yīng)一個內(nèi)核線程,。
內(nèi)核線程(Kernel Thread,KLT)就是直接由操作系統(tǒng)內(nèi)核支持的線程,,這種線程由內(nèi)核來完成線程切換,,內(nèi)核通過操作調(diào)度器對線程進行調(diào)度,并負責(zé)將線程的任務(wù)映射到各個處理器上,。一般一個處理核心對應(yīng)一個內(nèi)核線程,,比如單核處理器對應(yīng)一個內(nèi)核線程,雙核處理器對應(yīng)兩個內(nèi)核線程,,四核處理器對應(yīng)四個內(nèi)核線程,。
現(xiàn)在的電腦一般是雙核四線程、四核八線程,,是采用超線程技術(shù)將一個物理處理核心模擬成兩個邏輯處理核心,,對應(yīng)兩個內(nèi)核線程,所以在操作系統(tǒng)中看到的CPU數(shù)量是實際物理CPU數(shù)量的兩倍,,如你的電腦是雙核四線程,,打開“任務(wù)管理器\性能”可以看到4個CPU的監(jiān)視器,四核八線程可以看到8個CPU的監(jiān)視器,。
超線程技術(shù)就是利用特殊的硬件指令,,把一個物理芯片模擬成兩個邏輯處理核心,,讓單個處理器都能使用線程級并行計算,進而兼容多線程操作系統(tǒng)和軟件,,減少了CPU的閑置時間,,提高的CPU的運行效率。這種超線程技術(shù)(如雙核四線程)由處理器硬件的決定,,同時也需要操作系統(tǒng)的支持才能在計算機中表現(xiàn)出來,。
程序一般不會直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級接口——輕量級進程(Lightweight Process,,LWP),,輕量級進程就是我們通常意義上所講的線程,也被叫做用戶線程,。由于每個輕量級進程都由一個內(nèi)核線程支持,,因此只有先支持內(nèi)核線程,才能有輕量級進程,。用戶線程與內(nèi)核線程的對應(yīng)關(guān)系有三種模型:一對一模型,、多對一模型、多對多模型,,在這以4個內(nèi)核線程,、3個用戶線程為例對三種模型進行說明。
對于一對一模型來說,,一個用戶線程就唯一地對應(yīng)一個內(nèi)核線程(反過來不一定成立,,一個內(nèi)核線程不一定有對應(yīng)的用戶線程)。這樣,,如果CPU沒有采用超線程技術(shù)(如四核四線程的計算機),,一個用戶線程就唯一地映射到一個物理CPU的內(nèi)核線程,線程之間的并發(fā)是真正的并發(fā),。一對一模型使用戶線程具有與內(nèi)核線程一樣的優(yōu)點,,一個線程因某種原因阻塞時其他線程的執(zhí)行不受影響;此處,,一對一模型也可以讓多線程程序在多處理器的系統(tǒng)上有更好的表現(xiàn),。
但一對一模型也有兩個缺點:
多對一模型將多個用戶線程映射到一個內(nèi)核線程上,,線程之間的切換由用戶態(tài)的代碼來進行,系統(tǒng)內(nèi)核感受不到線程的實現(xiàn)方式,。用戶線程的建立,、同步,、銷毀等都在用戶態(tài)中完成,不需要內(nèi)核的介入,。因此相對一對一模型,,多對一模型的線程上下文切換速度要快許多;此外,,多對一模型對用戶線程的數(shù)量幾乎無限制,。
但多對一模型也有兩個缺點:
多對多模型結(jié)合了一對一模型和多對一模型的優(yōu)點,將多個用戶線程映射到多個內(nèi)核線程上,。由線程庫負責(zé)在可用的可調(diào)度實體上調(diào)度用戶線程,,這使得線程的上下文切換非常快,,因為它避免了系統(tǒng)調(diào)用,。但是增加了復(fù)雜性和優(yōu)先級倒置的可能性,以及在用戶態(tài)調(diào)度程序和內(nèi)核調(diào)度程序之間沒有廣泛(且高昂)協(xié)調(diào)的次優(yōu)調(diào)度,。
多對多模型的優(yōu)點有:
在現(xiàn)在流行的操作系統(tǒng)中,大都采用多對多的模型,。
一個應(yīng)用程序可能是多線程的,,也可能是多進程的,如何查看呢,?在Windows下我們只須打開任務(wù)管理器就能查看一個應(yīng)用程序的進程和線程數(shù),。按“Ctrl+Alt+Del”或右鍵快捷工具欄打開任務(wù)管理器。
查看進程數(shù)和線程數(shù):
在“進程”選項卡下,,我們可以看到一個應(yīng)用程序包含的線程數(shù),。如果一個應(yīng)用程序有多個進程,我們能看到每一個進程,,如在上圖中,,Google的Chrome瀏覽器就有多個進程,。同時,如果打開了一個應(yīng)用程序的多個實例也會有多個進程,,如上圖中我打開了兩個cmd窗口,,就有兩個cmd進程。如果看不到線程數(shù)這一列,,可以再點擊“查看\選擇列”菜單,,增加監(jiān)聽的列。
查看CPU和內(nèi)存的使用率:
在性能選項卡中,,我們可以查看CPU和內(nèi)存的使用率,,根據(jù)CPU使用記錄的監(jiān)視器的個數(shù)還能看出邏輯處理核心的個數(shù),如我的雙核四線程的計算機就有四個監(jiān)視器,。
當(dāng)線程的數(shù)量小于處理器的數(shù)量時,,線程的并發(fā)是真正的并發(fā),不同的線程運行在不同的處理器上,。但當(dāng)線程的數(shù)量大于處理器的數(shù)量時,,線程的并發(fā)會受到一些阻礙,此時并不是真正的并發(fā),,因為此時至少有一個處理器會運行多個線程,。
在單個處理器運行多個線程時,并發(fā)是一種模擬出來的狀態(tài),。操作系統(tǒng)采用時間片輪轉(zhuǎn)的方式輪流執(zhí)行每一個線程?,F(xiàn)在,幾乎所有的現(xiàn)代操作系統(tǒng)采用的都是時間片輪轉(zhuǎn)的搶占式調(diào)度方式,,如我們熟悉的Unix,、Linux、Windows及macOS等流行的操作系統(tǒng),。
我們知道線程是程序執(zhí)行的最小單位,,也是任務(wù)執(zhí)行的最小單位。在早期只有進程的操作系統(tǒng)中,,進程有五種狀態(tài),,創(chuàng)建、就緒,、運行,、阻塞(等待)、退出,。早期的進程相當(dāng)于現(xiàn)在的只有單個線程的進程,,那么現(xiàn)在的多線程也有五種狀態(tài),現(xiàn)在的多線程的生命周期與早期進程的生命周期類似,。
進程在運行過程有三種狀態(tài):就緒,、運行,、阻塞,創(chuàng)建和退出狀態(tài)描述的是進程的創(chuàng)建過程和退出過程,。
協(xié)程,英文Coroutines,,是一種基于線程之上,,但又比線程更加輕量級的存在,這種由程序員自己寫程序來管理的輕量級線程叫做『用戶空間線程』,,具有對內(nèi)核來說不可見的特性,。
因為是自主開辟的異步任務(wù),所以很多人也更喜歡叫它們纖程(Fiber),,或者綠色線程(GreenThread),。正如一個進程可以擁有多個線程一樣,,一個線程也可以擁有多個協(xié)程。
在傳統(tǒng)的J2EE系統(tǒng)中都是基于每個請求占用一個線程去完成完整的業(yè)務(wù)邏輯(包括事務(wù)),。所以系統(tǒng)的吞吐能力取決于每個線程的操作耗時,。如果遇到很耗時的I/O行為,則整個系統(tǒng)的吞吐立刻下降,,因為這個時候線程一直處于阻塞狀態(tài),,如果線程很多的時候,會存在很多線程處于空閑狀態(tài)(等待該線程執(zhí)行完才能執(zhí)行),,造成了資源應(yīng)用不徹底,。
最常見的例子就是JDBC(它是同步阻塞的),這也是為什么很多人都說數(shù)據(jù)庫是瓶頸的原因,。這里的耗時其實是讓CPU一直在等待I/O返回,,說白了線程根本沒有利用CPU去做運算,而是處于空轉(zhuǎn)狀態(tài),。而另外過多的線程,,也會帶來更多的ContextSwitch開銷。
對于上述問題,,現(xiàn)階段行業(yè)里的比較流行的解決方案之一就是單線程加上異步回調(diào),。其代表派是node.js以及Java里的新秀Vert.x。
而協(xié)程的目的就是當(dāng)出現(xiàn)長時間的I/O操作時,,通過讓出目前的協(xié)程調(diào)度,,執(zhí)行下一個任務(wù)的方式,來消除ContextSwitch上的開銷,。
當(dāng)出現(xiàn)IO阻塞的時候,,由協(xié)程的調(diào)度器進行調(diào)度,,通過將數(shù)據(jù)流立刻yield掉(主動讓出),并且記錄當(dāng)前棧上的數(shù)據(jù),,阻塞完后立刻再通過線程恢復(fù)棧,,并把阻塞的結(jié)果放到這個線程上去跑,這樣看上去好像跟寫同步代碼沒有任何差別,,這整個流程可以稱為coroutine,,而跑在由coroutine
負責(zé)調(diào)度的線程稱為Fiber
。比如Golang里的 go關(guān)鍵字其實就是負責(zé)開啟一個Fiber
,,讓func
邏輯跑在上面,。
由于協(xié)程的暫停完全由程序控制,發(fā)生在用戶態(tài)上,;而線程的阻塞狀態(tài)是由操作系統(tǒng)內(nèi)核來進行切換,,發(fā)生在內(nèi)核態(tài)上。
因此,,協(xié)程的開銷遠遠小于線程的開銷,,也就沒有了ContextSwitch上的開銷。
比較項 | 線程 | 協(xié)程 |
---|---|---|
占用資源 | 初始單位為1MB,固定不可變 | 初始一般為 2KB,,可隨需要而增大 |
調(diào)度所屬 | 由 OS 的內(nèi)核完成 | 由用戶完成 |
切換開銷 | 涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài)),、16個寄存器、PC,、SP...等寄存器的刷新等 | 只有三個寄存器的值修改 - PC / SP / DX. |
性能問題 | 資源占用太高,,頻繁創(chuàng)建銷毀會帶來嚴(yán)重的性能問題 | 資源占用小,不會帶來嚴(yán)重的性能問題 |
數(shù)據(jù)同步 | 需要用鎖等機制確保數(shù)據(jù)的一直性和可見性 | 不需要多線程的鎖機制,因為只有一個線程,,也不存在同時寫變量沖突,,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,,所以執(zhí)行效率比多線程高很多,。 |