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