成都創(chuàng)新互聯(lián)公司長(zhǎng)期為成百上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開(kāi)放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為閬中企業(yè)提供專業(yè)的網(wǎng)站制作、成都網(wǎng)站建設(shè),閬中網(wǎng)站改版等技術(shù)服務(wù)。擁有十年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開(kāi)發(fā)。
坐標(biāo)上海松江高科技園,誠(chéng)聘高級(jí)前端工程師/高級(jí) Java 工程師,有興趣的看 JD:https://www.lagou.com/jobs/6361564.html
在 《Awesome Interviews》 歸納的常見(jiàn)面試題中,無(wú)論前后端,并發(fā)與異步的相關(guān)知識(shí)都是面試的中重中之重,《并發(fā)編程》系列即對(duì)于面試中常見(jiàn)的并發(fā)知識(shí)再進(jìn)行回顧總結(jié);你也可以前往 《Awesome Interviews》,在實(shí)際的面試題考校中了解自己的掌握程度。也可以前往《Java 實(shí)戰(zhàn)》、《Go 實(shí)戰(zhàn)》等了解具體編程語(yǔ)言中的并發(fā)編程的相關(guān)知識(shí)。
在未配置 OS 的系統(tǒng)中,程序的執(zhí)行方式是順序執(zhí)行,即必須在一個(gè)程序執(zhí)行完后,才允許另一個(gè)程序執(zhí)行;在多道程序環(huán)境下,則允許多個(gè)程序并發(fā)執(zhí)行。程序的這兩種執(zhí)行方式間有著顯著的不同。也正是程序并發(fā)執(zhí)行時(shí)的這種特征,才導(dǎo)致了在操作系統(tǒng)中引入進(jìn)程的概念。進(jìn)程是資源分配的基本單位,線程是資源調(diào)度的基本單位。
應(yīng)用啟動(dòng)體現(xiàn)的就是靜態(tài)指令加載進(jìn)內(nèi)存,進(jìn)而進(jìn)入 CPU 運(yùn)算,操作系統(tǒng)在內(nèi)存開(kāi)辟了一段棧內(nèi)存用來(lái)存放指令和變量值,從而形成了進(jìn)程。早期的操作系統(tǒng)基于進(jìn)程來(lái)調(diào)度 CPU,不同進(jìn)程間是不共享內(nèi)存空間的,所以進(jìn)程要做任務(wù)切換就要切換內(nèi)存映射地址。由于進(jìn)程的上下文關(guān)聯(lián)的變量,引用,計(jì)數(shù)器等現(xiàn)場(chǎng)數(shù)據(jù)占用了打段的內(nèi)存空間,所以頻繁切換進(jìn)程需要整理一大段內(nèi)存空間來(lái)保存未執(zhí)行完的進(jìn)程現(xiàn)場(chǎng),等下次輪到 CPU 時(shí)間片再恢復(fù)現(xiàn)場(chǎng)進(jìn)行運(yùn)算。
這樣既耗費(fèi)時(shí)間又浪費(fèi)空間,所以我們才要研究多線程。一個(gè)進(jìn)程創(chuàng)建的所有線程,都是共享一個(gè)內(nèi)存空間的,所以線程做任務(wù)切換成本就很低了?,F(xiàn)代的操作系統(tǒng)都基于更輕量的線程來(lái)調(diào)度,現(xiàn)在我們提到的“任務(wù)切換”都是指“線程切換”。
本部分節(jié)選自 《Linux 與操作系統(tǒng)/進(jìn)程管理》。
在未配置 OS 的系統(tǒng)中,程序的執(zhí)行方式是順序執(zhí)行,即必須在一個(gè)程序執(zhí)行完后,才允許另一個(gè)程序執(zhí)行;在多道程序環(huán)境下,則允許多個(gè)程序并發(fā)執(zhí)行。程序的這兩種執(zhí)行方式間有著顯著的不同。也正是程序并發(fā)執(zhí)行時(shí)的這種特征,才導(dǎo)致了在操作系統(tǒng)中引入進(jìn)程的概念。進(jìn)程是資源分配的基本單位,線程是資源調(diào)度的基本單位。
進(jìn)程是操作系統(tǒng)對(duì)一個(gè)正在運(yùn)行的程序的一種抽象,在一個(gè)系統(tǒng)上可以同時(shí)運(yùn)行多個(gè)進(jìn)程,而每個(gè)進(jìn)程都好像在獨(dú)占地使用硬件。所謂的并發(fā)運(yùn)行,則是說(shuō)一個(gè)進(jìn)程的指令和另一個(gè)進(jìn)程的指令是交錯(cuò)執(zhí)行的。無(wú)論是在單核還是多核系統(tǒng)中,可以通過(guò)處理器在進(jìn)程間切換,來(lái)實(shí)現(xiàn)單個(gè) CPU 看上去像是在并發(fā)地執(zhí)行多個(gè)進(jìn)程。操作系統(tǒng)實(shí)現(xiàn)這種交錯(cuò)執(zhí)行的機(jī)制稱為上下文切換。
操作系統(tǒng)保持跟蹤進(jìn)程運(yùn)行所需的所有狀態(tài)信息。這種狀態(tài),也就是上下文,它包括許多信息,例如 PC 和寄存器文件的當(dāng)前值,以及主存的內(nèi)容。在任何一個(gè)時(shí)刻,單處理器系統(tǒng)都只能執(zhí)行一個(gè)進(jìn)程的代碼。當(dāng)操作系統(tǒng)決定要把控制權(quán)從當(dāng)前進(jìn)程轉(zhuǎn)移到某個(gè)新進(jìn)程時(shí),就會(huì)進(jìn)行上下文切換,即保存當(dāng)前進(jìn)程的上下文、恢復(fù)新進(jìn)程的上下文,然后將控制權(quán)傳遞到新進(jìn)程。新進(jìn)程就會(huì)從上次停止的地方開(kāi)始。
在虛擬存儲(chǔ)管理一節(jié)中,我們介紹過(guò)它為每個(gè)進(jìn)程提供了一個(gè)假象,即每個(gè)進(jìn)程都在獨(dú)占地使用主存。每個(gè)進(jìn)程看到的是一致的存儲(chǔ)器,稱為虛擬地址空間。其虛擬地址空間最上面的區(qū)域是為操作系統(tǒng)中的代碼和數(shù)據(jù)保留的,這對(duì)所有進(jìn)程來(lái)說(shuō)都是一樣的;地址空間的底部區(qū)域存放用戶進(jìn)程定義的代碼和數(shù)據(jù)。
程序代碼和數(shù)據(jù),對(duì)于所有的進(jìn)程來(lái)說(shuō),代碼是從同一固定地址開(kāi)始,直接按照可執(zhí)行目標(biāo)文件的內(nèi)容初始化。
堆,代碼和數(shù)據(jù)區(qū)后緊隨著的是運(yùn)行時(shí)堆。代碼和數(shù)據(jù)區(qū)是在進(jìn)程一開(kāi)始運(yùn)行時(shí)就被規(guī)定了大小,與此不同,當(dāng)調(diào)用如 malloc 和 free 這樣的 C 標(biāo)準(zhǔn)庫(kù)函數(shù)時(shí),堆可以在運(yùn)行時(shí)動(dòng)態(tài)地?cái)U(kuò)展和收縮。
共享庫(kù):大約在地址空間的中間部分是一塊用來(lái)存放像 C 標(biāo)準(zhǔn)庫(kù)和數(shù)學(xué)庫(kù)這樣共享庫(kù)的代碼和數(shù)據(jù)的區(qū)域。
棧,位于用戶虛擬地址空間頂部的是用戶棧,編譯器用它來(lái)實(shí)現(xiàn)函數(shù)調(diào)用。和堆一樣,用戶棧在程序執(zhí)行期間可以動(dòng)態(tài)地?cái)U(kuò)展和收縮。
在現(xiàn)代系統(tǒng)中,一個(gè)進(jìn)程實(shí)際上可以由多個(gè)稱為線程的執(zhí)行單元組成,每個(gè)線程都運(yùn)行在進(jìn)程的上下文中,并共享同樣的代碼和全局?jǐn)?shù)據(jù)。進(jìn)程的個(gè)體間是完全獨(dú)立的,而線程間是彼此依存的。多進(jìn)程環(huán)境中,任何一個(gè)進(jìn)程的終止,不會(huì)影響到其他進(jìn)程。而多線程環(huán)境中,父線程終止,全部子線程被迫終止(沒(méi)有了資源)。
而任何一個(gè)子線程終止一般不會(huì)影響其他線程,除非子線程執(zhí)行了 exit()
系統(tǒng)調(diào)用。任何一個(gè)子線程執(zhí)行 exit()
,全部線程同時(shí)滅亡。多線程程序中至少有一個(gè)主線程,而這個(gè)主線程其實(shí)就是有 main 函數(shù)的進(jìn)程。它是整個(gè)程序的進(jìn)程,所有線程都是它的子線程;我們通常把具有多線程的主進(jìn)程稱之為主線程。
線程共享的環(huán)境包括:進(jìn)程代碼段、進(jìn)程的公有數(shù)據(jù)、進(jìn)程打開(kāi)的文件描述符、信號(hào)的處理器、進(jìn)程的當(dāng)前目錄、進(jìn)程用戶 ID 與進(jìn)程組 ID 等,利用這些共享的數(shù)據(jù),線程很容易的實(shí)現(xiàn)相互之間的通訊。線程擁有這許多共性的同時(shí),還擁有自己的個(gè)性,并以此實(shí)現(xiàn)并發(fā)性:
線程 ID:每個(gè)線程都有自己的線程 ID,這個(gè) ID 在本進(jìn)程中是唯一的。進(jìn)程用此來(lái)標(biāo)識(shí)線程。
寄存器組的值:由于線程間是并發(fā)運(yùn)行的,每個(gè)線程有自己不同的運(yùn)行線索,當(dāng)從一個(gè)線程切換到另一個(gè)線程上時(shí),必須將原有的線程的寄存器集合的狀態(tài)保存,以便 將來(lái)該線程在被重新切換到時(shí)能得以恢復(fù)。
線程的堆棧:堆棧是保證線程獨(dú)立運(yùn)行所必須的。線程函數(shù)可以調(diào)用函數(shù),而被調(diào)用函數(shù)中又是可以層層嵌套的,所以線程必須擁有自己的函數(shù)堆棧, 使得函數(shù)調(diào)用可以正常執(zhí)行,不受其他線程的影響。
錯(cuò)誤返回碼:由于同一個(gè)進(jìn)程中有很多個(gè)線程在同時(shí)運(yùn)行,可能某個(gè)線程進(jìn)行系統(tǒng)調(diào)用后設(shè)置了 errno 值,而在該 線程還沒(méi)有處理這個(gè)錯(cuò)誤,另外一個(gè)線程就在此時(shí) 被調(diào)度器投入運(yùn)行,這樣錯(cuò)誤值就有可能被修改。 所以,不同的線程應(yīng)該擁有自己的錯(cuò)誤返回碼變量。
線程的信號(hào)屏蔽碼:由于每個(gè)線程所感興趣的信號(hào)不同,所以線程的信號(hào)屏蔽碼應(yīng)該由線程自己管理。但所有的線程都共享同樣的信號(hào)處理器。
當(dāng)線程在用戶空間下實(shí)現(xiàn)時(shí),操作系統(tǒng)對(duì)線程的存在一無(wú)所知,操作系統(tǒng)只能看到進(jìn)程,而不能看到線程。所有的線程都是在用戶空間實(shí)現(xiàn)。在操作系統(tǒng)看來(lái),每一個(gè)進(jìn)程只有一個(gè)線程。過(guò)去的操作系統(tǒng)大部分是這種實(shí)現(xiàn)方式,這種方式的好處之一就是即使操作系統(tǒng)不支持線程,也可以通過(guò)庫(kù)函數(shù)來(lái)支持線程。
在這在模型下,程序員需要自己實(shí)現(xiàn)線程的數(shù)據(jù)結(jié)構(gòu)、創(chuàng)建銷毀和調(diào)度維護(hù)。也就相當(dāng)于需要實(shí)現(xiàn)一個(gè)自己的線程調(diào)度內(nèi)核,而同時(shí)這些線程運(yùn)行在操作系統(tǒng)的一個(gè)進(jìn)程內(nèi),最后操作系統(tǒng)直接對(duì)進(jìn)程進(jìn)行調(diào)度。
這樣做有一些優(yōu)點(diǎn),首先就是確實(shí)在操作系統(tǒng)中實(shí)現(xiàn)了真實(shí)的多線程,其次就是線程的調(diào)度只是在用戶態(tài),減少了操作系統(tǒng)從內(nèi)核態(tài)到用戶態(tài)的切換開(kāi)銷。這種模式最致命的缺點(diǎn)也是由于操作系統(tǒng)不知道線程的存在,因此當(dāng)一個(gè)進(jìn)程中的某一個(gè)線程進(jìn)行系統(tǒng)調(diào)用時(shí),比如缺頁(yè)中斷而導(dǎo)致線程阻塞,此時(shí)操作系統(tǒng)會(huì)阻塞整個(gè)進(jìn)程,即使這個(gè)進(jìn)程中其它線程還在工作。還有一個(gè)問(wèn)題是假如進(jìn)程中一個(gè)線程長(zhǎng)時(shí)間不釋放 CPU,因?yàn)橛脩艨臻g并沒(méi)有時(shí)鐘中斷機(jī)制,會(huì)導(dǎo)致此進(jìn)程中的其它線程得不到 CPU 而持續(xù)等待。
內(nèi)核線程就是直接由操作系統(tǒng)內(nèi)核(Kernel)支持的線程,這種線程由內(nèi)核來(lái)完成線程切換,內(nèi)核通過(guò)操縱調(diào)度器(Scheduler)對(duì)線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上。每個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身,這樣操作系統(tǒng)就有能力同時(shí)處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核(Multi-Threads Kernel)。
程序員直接使用操作系統(tǒng)中已經(jīng)實(shí)現(xiàn)的線程,而線程的創(chuàng)建、銷毀、調(diào)度和維護(hù),都是靠操作系統(tǒng)(準(zhǔn)確的說(shuō)是內(nèi)核)來(lái)實(shí)現(xiàn),程序員只需要使用系統(tǒng)調(diào)用,而不需要自己設(shè)計(jì)線程的調(diào)度算法和線程對(duì) CPU 資源的搶占使用。
在這種混合實(shí)現(xiàn)下,即存在用戶線程,也存在輕量級(jí)進(jìn)程。用戶線程還是完全建立在用戶空間中,因此用戶線程的創(chuàng)建、切換、析構(gòu)等操作依然廉價(jià),并且可以支持大規(guī)模的用戶線程并發(fā)。而操作系統(tǒng)提供支持的輕量級(jí)進(jìn)程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過(guò)輕量級(jí)進(jìn)程來(lái)完成,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn)。在這種混合模式中,用戶線程與輕量級(jí)進(jìn)程的數(shù)量比是不定的,即為 N:M 的關(guān)系:
Golang 的協(xié)程就是使用了這種模型,在用戶態(tài),協(xié)程能快速的切換,避免了線程調(diào)度的 CPU 開(kāi)銷問(wèn)題,協(xié)程相當(dāng)于線程的線程。
在 Linux 2.4 版以前,線程的實(shí)現(xiàn)和管理方式就是完全按照進(jìn)程方式實(shí)現(xiàn)的;在 Linux 2.6 之前,內(nèi)核并不支持線程的概念,僅通過(guò)輕量級(jí)進(jìn)程(Lightweight Process)模擬線程;輕量級(jí)進(jìn)程是建立在內(nèi)核之上并由內(nèi)核支持的用戶線程,它是內(nèi)核線程的高度抽象,每一個(gè)輕量級(jí)進(jìn)程都與一個(gè)特定的內(nèi)核線程關(guān)聯(lián)。內(nèi)核線程只能由內(nèi)核管理并像普通進(jìn)程一樣被調(diào)度。這種模型最大的特點(diǎn)是線程調(diào)度由內(nèi)核完成了,而其他線程操作(同步、取消)等都是核外的線程庫(kù)(Linux Thread)函數(shù)完成的。
為了完全兼容 Posix 標(biāo)準(zhǔn),Linux 2.6 首先對(duì)內(nèi)核進(jìn)行了改進(jìn),引入了線程組的概念(仍然用輕量級(jí)進(jìn)程表示線程),有了這個(gè)概念就可以將一組線程組織稱為一個(gè)進(jìn)程,不過(guò)內(nèi)核并沒(méi)有準(zhǔn)備特別的調(diào)度算法或是定義特別的數(shù)據(jù)結(jié)構(gòu)來(lái)表征線程;相反,線程僅僅被視為一個(gè)與其他進(jìn)程(概念上應(yīng)該是線程)共享某些資源的進(jìn)程(概念上應(yīng)該是線程)。在實(shí)現(xiàn)上主要的改變就是在 task_struct 中加入 tgid 字段,這個(gè)字段就是用于表示線程組 id 的字段。在用戶線程庫(kù)方面,也使用 NPTL 代替 Linux Thread,不同調(diào)度模型上仍然采用 1 對(duì) 1
模型。
進(jìn)程的實(shí)現(xiàn)是調(diào)用 fork 系統(tǒng)調(diào)用:pid_t fork(void);
,線程的實(shí)現(xiàn)是調(diào)用 clone 系統(tǒng)調(diào)用:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...)
。與標(biāo)準(zhǔn) fork()
相比,線程帶來(lái)的開(kāi)銷非常小,內(nèi)核無(wú)需單獨(dú)復(fù)制進(jìn)程的內(nèi)存空間或文件描寫(xiě)敘述符等等。這就節(jié)省了大量的 CPU 時(shí)間,使得線程創(chuàng)建比新進(jìn)程創(chuàng)建快上十到一百倍,能夠大量使用線程而無(wú)需太過(guò)于操心帶來(lái)的 CPU 或內(nèi)存不足。無(wú)論是 fork、vfork、kthread_create 最后都是要調(diào)用 do_fork,而 do_fork 就是根據(jù)不同的函數(shù)參數(shù),對(duì)一個(gè)進(jìn)程所需的資源進(jìn)行分配。
內(nèi)核線程是由內(nèi)核自己創(chuàng)建的線程,也叫做守護(hù)線程(Deamon),在終端上用命令 ps -Al
列出的所有進(jìn)程中,名字以 k 開(kāi)關(guān)以 d 結(jié)尾的往往都是內(nèi)核線程,比如 kthreadd、kswapd 等。與用戶線程相比,它們都由 do_fork()
創(chuàng)建,每個(gè)線程都有獨(dú)立的 task_struct 和內(nèi)核棧;也都參與調(diào)度,內(nèi)核線程也有優(yōu)先級(jí),會(huì)被調(diào)度器平等地?fù)Q入換出。二者的不同之處在于,內(nèi)核線程只工作在內(nèi)核態(tài)中;而用戶線程則既可以運(yùn)行在內(nèi)核態(tài)(執(zhí)行系統(tǒng)調(diào)用時(shí)),也可以運(yùn)行在用戶態(tài);內(nèi)核線程沒(méi)有用戶空間,所以對(duì)于一個(gè)內(nèi)核線程來(lái)說(shuō),它的 0~3G 的內(nèi)存空間是空白的,它的 current->mm
是空的,與內(nèi)核使用同一張頁(yè)表;而用戶線程則可以看到完整的 0~4G 內(nèi)存空間。
在 Linux 內(nèi)核啟動(dòng)的最后階段,系統(tǒng)會(huì)創(chuàng)建兩個(gè)內(nèi)核線程,一個(gè)是 init,一個(gè)是 kthreadd。其中 init 線程的作用是運(yùn)行文件系統(tǒng)上的一系列”init”腳本,并啟動(dòng) shell 進(jìn)程,所以 init 線程稱得上是系統(tǒng)中所有用戶進(jìn)程的祖先,它的 pid 是 1。kthreadd 線程是內(nèi)核的守護(hù)線程,在內(nèi)核正常工作時(shí),它永遠(yuǎn)不退出,是一個(gè)死循環(huán),它的 pid 是 2。
協(xié)程是用戶模式下的輕量級(jí)線程,最準(zhǔn)確的名字應(yīng)該叫用戶空間線程(User Space Thread),在不同的領(lǐng)域中也有不同的叫法,譬如纖程(Fiber)、綠色線程(Green Thread)等等。操作系統(tǒng)內(nèi)核對(duì)協(xié)程一無(wú)所知,協(xié)程的調(diào)度完全有應(yīng)用程序來(lái)控制,操作系統(tǒng)不管這部分的調(diào)度;一個(gè)線程可以包含一個(gè)或多個(gè)協(xié)程,協(xié)程擁有自己的寄存器上下文和棧,協(xié)程調(diào)度切換時(shí),將寄存器上細(xì)紋和棧保存起來(lái),在切換回來(lái)時(shí)恢復(fù)先前保運(yùn)的寄存上下文和棧。
協(xié)程的優(yōu)勢(shì)如下:
比如 Golang 里的 go 關(guān)鍵字其實(shí)就是負(fù)責(zé)開(kāi)啟一個(gè) Fiber,讓 func 邏輯跑在上面。而這一切都是發(fā)生的用戶態(tài)上,沒(méi)有發(fā)生在內(nèi)核態(tài)上,也就是說(shuō)沒(méi)有 ContextSwitch 上的開(kāi)銷。協(xié)程的實(shí)現(xiàn)庫(kù)中筆者較為常用的譬如 Go Routine、node-fibers、Java-Quasar 等。
Go 線程模型屬于多對(duì)多線程模型,在操作系統(tǒng)提供的內(nèi)核線程之上,Go 搭建了一個(gè)特有的兩級(jí)線程模型。Go 中使用使用 Go 語(yǔ)句創(chuàng)建的 Goroutine 可以認(rèn)為是輕量級(jí)的用戶線程,Go 線程模型包含三個(gè)概念:
G: 表示 Goroutine,每個(gè) Goroutine 對(duì)應(yīng)一個(gè) G 結(jié)構(gòu)體,G 存儲(chǔ) Goroutine 的運(yùn)行堆棧、狀態(tài)以及任務(wù)函數(shù),可重用。G 并非執(zhí)行體,每個(gè) G 需要綁定到 P 才能被調(diào)度執(zhí)行。
P: Processor,表示邏輯處理器,對(duì) G 來(lái)說(shuō),P 相當(dāng)于 CPU 核,G 只有綁定到 P(在 P 的 local runq 中)才能被調(diào)度。對(duì) M 來(lái)說(shuō),P 提供了相關(guān)的執(zhí)行環(huán)境(Context),如內(nèi)存分配狀態(tài)(mcache),任務(wù)隊(duì)列(G)等,P 的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的 G 的數(shù)量(物理 CPU 核數(shù) >= P 的數(shù)量),P 的數(shù)量由用戶設(shè)置的 GOMAXPROCS 決定,但是不論 GOMAXPROCS 設(shè)置為多大,P 的數(shù)量最大為 256。
在 Go 中每個(gè)邏輯處理器(P)會(huì)綁定到某一個(gè)內(nèi)核線程上,每個(gè)邏輯處理器(P)內(nèi)有一個(gè)本地隊(duì)列,用來(lái)存放 Go 運(yùn)行時(shí)分配的 goroutine。多對(duì)多線程模型中是操作系統(tǒng)調(diào)度線程在物理 CPU 上運(yùn)行,在 Go 中則是 Go 的運(yùn)行時(shí)調(diào)度 Goroutine 在邏輯處理器(P)上運(yùn)行。
Go 的棧是動(dòng)態(tài)分配大小的,隨著存儲(chǔ)數(shù)據(jù)的數(shù)量而增長(zhǎng)和收縮。每個(gè)新建的 Goroutine 只有大約 4KB 的棧。每個(gè)棧只有 4KB,那么在一個(gè) 1GB 的 RAM 上,我們就可以有 256 萬(wàn)個(gè) Goroutine 了,相對(duì)于 Java 中每個(gè)線程的 1MB,這是巨大的提升。Golang 實(shí)現(xiàn)了自己的調(diào)度器,允許眾多的 Goroutines 運(yùn)行在相同的 OS 線程上。就算 Go 會(huì)運(yùn)行與內(nèi)核相同的上下文切換,但是它能夠避免切換至 ring-0 以運(yùn)行內(nèi)核,然后再切換回來(lái),這樣就會(huì)節(jié)省大量的時(shí)間。
在 Go 中存在兩級(jí)調(diào)度:
使用 Go 語(yǔ)句創(chuàng)建一個(gè) Goroutine 后,創(chuàng)建的 Goroutine 會(huì)被放入 Go 運(yùn)行時(shí)調(diào)度器的全局運(yùn)行隊(duì)列中,然后 Go 運(yùn)行時(shí)調(diào)度器會(huì)把全局隊(duì)列中的 Goroutine 分配給不同的邏輯處理器(P),分配的 Goroutine 會(huì)被放到邏輯處理器(P)的本地隊(duì)列中,當(dāng)本地隊(duì)列中某個(gè) Goroutine 就緒后待分配到時(shí)間片后就可以在邏輯處理器上運(yùn)行了。
目前,JVM 本身并未提供協(xié)程的實(shí)現(xiàn)庫(kù),像 Quasar 這樣的協(xié)程框架似乎也仍非主流的并發(fā)問(wèn)題解決方案,在本部分我們就討論下在 Java 中是否有必要一定要引入?yún)f(xié)程。在普通的 Web 服務(wù)器場(chǎng)景下,譬如 Spring Boot 中默認(rèn)的 Worker 線程池線程數(shù)在 200(50 ~ 500) 左右,如果從線程的內(nèi)存占用角度來(lái)考慮,每個(gè)線程上下文約 128KB,那么 500 個(gè)線程本身的內(nèi)存占用在 60M,相較于整個(gè)堆棧不過(guò)爾爾。而 Java 本身提供的線程池,對(duì)于線程的創(chuàng)建與銷毀都有非常好的支持;即使 Vert.x 或 Kotlin 中提供的協(xié)程,往往也是基于原生線程池實(shí)現(xiàn)的。
從線程的切換開(kāi)銷的角度來(lái)看,我們常說(shuō)的切換開(kāi)銷往往是針對(duì)于活躍線程;而普通的 Web 服務(wù)器天然會(huì)有大量的線程因?yàn)檎?qǐng)求讀寫(xiě)、DB 讀寫(xiě)這樣的操作而掛起,實(shí)際只有數(shù)十個(gè)并發(fā)活躍線程會(huì)參與到 OS 的線程切換調(diào)度。而如果真的存在著大量活躍線程的場(chǎng)景,Java 生態(tài)圈中也存在了 Akka 這樣的 Actor 并發(fā)模型框架,它能夠感知線程何時(shí)能夠執(zhí)行工作,在用戶空間中構(gòu)建運(yùn)行時(shí)調(diào)度器,從而支持百萬(wàn)級(jí)別的 Actor 并發(fā)。
實(shí)際上我們引入?yún)f(xié)程的場(chǎng)景,更多的是面對(duì)所謂百萬(wàn)級(jí)別連接的處理,典型的就是 IM 服務(wù)器,可能需要同時(shí)處理大量空閑的鏈接。此時(shí)在 Java 生態(tài)圈中,我們可以使用 Netty 去進(jìn)行處理,其基于 NIO 與 Worker Thread 實(shí)現(xiàn)的調(diào)度機(jī)制就很類似于協(xié)程,可以解決絕大部分因?yàn)?IO 的等待造成資源浪費(fèi)的問(wèn)題。而從并發(fā)模型對(duì)比的角度,如果我們希望能遵循 Go 中以消息傳遞方式實(shí)現(xiàn)內(nèi)存共享的理念,那么也可以采用 Disruptor 這樣的模型。
Java 線程在 JDK1.2 之前,是基于稱為“綠色線程”(Green Threads)的用戶線程實(shí)現(xiàn)的,而到了 JDK1.2 及以后,JVM 選擇了更加穩(wěn)健且方便使用的操作系統(tǒng)原生的線程模型,通過(guò)系統(tǒng)調(diào)用,將程序的線程交給了操作系統(tǒng)內(nèi)核進(jìn)行調(diào)度。因此,在目前的 JDK 版本中,操作系統(tǒng)支持怎樣的線程模型,在很大程度上決定了 Java 虛擬機(jī)的線程是怎樣映射的,這點(diǎn)在不同的平臺(tái)上沒(méi)有辦法達(dá)成一致,虛擬機(jī)規(guī)范中也并未限定 Java 線程需要使用哪種線程模型來(lái)實(shí)現(xiàn)。線程模型只對(duì)線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響,對(duì) Java 程序的編碼和運(yùn)行過(guò)程來(lái)說(shuō),這些差異都是透明的。
對(duì)于 Sun JDK 來(lái)說(shuō),它的 Windows 版與 Linux 版都是使用一對(duì)一的線程模型實(shí)現(xiàn)的,一條 Java 線程就映射到一條輕量級(jí)進(jìn)程之中,因?yàn)?Windows 和 Linux 系統(tǒng)提供的線程模型就是一對(duì)一的。也就是說(shuō),現(xiàn)在的 Java 中線程的本質(zhì),其實(shí)就是操作系統(tǒng)中的線程,Linux 下是基于 pthread 庫(kù)實(shí)現(xiàn)的輕量級(jí)進(jìn)程,Windows 下是原生的系統(tǒng) Win32 API 提供系統(tǒng)調(diào)用從而實(shí)現(xiàn)多線程。
在現(xiàn)在的操作系統(tǒng)中,因?yàn)榫€程依舊被視為輕量級(jí)進(jìn)程,所以操作系統(tǒng)中線程的狀態(tài)實(shí)際上和進(jìn)程狀態(tài)是一致的模型。從實(shí)際意義上來(lái)講,操作系統(tǒng)中的線程除去 new 和 terminated 狀態(tài),一個(gè)線程真實(shí)存在的狀態(tài),只有:
ready
:表示線程已經(jīng)被創(chuàng)建,正在等待系統(tǒng)調(diào)度分配 CPU 使用權(quán)。running
:表示線程獲得了 CPU 使用權(quán),正在進(jìn)行運(yùn)算。waiting
:表示線程等待(或者說(shuō)掛起),讓出 CPU 資源給其他線程使用。對(duì)于 Java 中的線程狀態(tài):無(wú)論是 Timed Waiting ,Waiting 還是 Blocked,對(duì)應(yīng)的都是操作系統(tǒng)線程的 waiting(等待)狀態(tài)。而 Runnable 狀態(tài),則對(duì)應(yīng)了操作系統(tǒng)中的 ready 和 running 狀態(tài)。Java 線程和操作系統(tǒng)線程,實(shí)際上同根同源,但又相差甚遠(yuǎn)。
您可以通過(guò)以下導(dǎo)航來(lái)在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術(shù)資料歸納、編程語(yǔ)言與理論、Web 與大前端、服務(wù)端開(kāi)發(fā)與基礎(chǔ)架構(gòu)、云計(jì)算與大數(shù)據(jù)、數(shù)據(jù)科學(xué)與人工智能、產(chǎn)品設(shè)計(jì)等多個(gè)領(lǐng)域:
知識(shí)體系:《Awesome Lists | CS 資料集錦》、《Awesome CheatSheets | 速學(xué)速查手冊(cè)》、《Awesome Interviews | 求職面試必備》、《Awesome RoadMaps | 程序員進(jìn)階指南》、《Awesome MindMaps | 知識(shí)脈絡(luò)思維腦圖》、《Awesome-CS-Books | 開(kāi)源書(shū)籍(.pdf)匯總》
編程語(yǔ)言:《編程語(yǔ)言理論》、《Java 實(shí)戰(zhàn)》、《JavaScript 實(shí)戰(zhàn)》、《Go 實(shí)戰(zhàn)》、《Python 實(shí)戰(zhàn)》、《Rust 實(shí)戰(zhàn)》
軟件工程、模式與架構(gòu):《編程范式與設(shè)計(jì)模式》、《數(shù)據(jù)結(jié)構(gòu)與算法》、《軟件架構(gòu)設(shè)計(jì)》、《整潔與重構(gòu)》、《研發(fā)方式與工具》
Web 與大前端:《現(xiàn)代 Web 開(kāi)發(fā)基礎(chǔ)與工程實(shí)踐》、《數(shù)據(jù)可視化》、《iOS》、《Android》、《混合開(kāi)發(fā)與跨端應(yīng)用》
服務(wù)端開(kāi)發(fā)實(shí)踐與工程架構(gòu):《服務(wù)端基礎(chǔ)》、《微服務(wù)與云原生》、《測(cè)試與高可用保障》、《DevOps》、《Node》、《Spring》、《信息安全與***測(cè)試》
分布式基礎(chǔ)架構(gòu):《分布式系統(tǒng)》、《分布式計(jì)算》、《數(shù)據(jù)庫(kù)》、《網(wǎng)絡(luò)》、《虛擬化與編排》、《云計(jì)算與大數(shù)據(jù)》、《Linux 與操作系統(tǒng)》
數(shù)據(jù)科學(xué),人工智能與深度學(xué)習(xí):《數(shù)理統(tǒng)計(jì)》、《數(shù)據(jù)分析》、《機(jī)器學(xué)習(xí)》、《深度學(xué)習(xí)》、《自然語(yǔ)言處理》、《工具與工程化》、《行業(yè)應(yīng)用》
產(chǎn)品設(shè)計(jì)與用戶體驗(yàn):《產(chǎn)品設(shè)計(jì)》、《交互體驗(yàn)》、《項(xiàng)目管理》
此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書(shū)籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項(xiàng)目源代碼等更詳細(xì)的目錄導(dǎo)航信息。最后,你也可以關(guān)注微信公眾號(hào):『某熊的技術(shù)之路』以獲取最新資訊。
當(dāng)前名稱:并發(fā)面試必備系列之進(jìn)程、線程與協(xié)程
本文URL:http://jinyejixie.com/article44/ipgohe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供自適應(yīng)網(wǎng)站、外貿(mào)建站、移動(dòng)網(wǎng)站建設(shè)、響應(yīng)式網(wǎng)站、靜態(tài)網(wǎng)站、網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)