線程 | 同步 | 異步 | 異構(gòu)
協(xié)程 | 進程 | 同構(gòu) | 線程池
當前,隨著“東數(shù)西算”政策的落地,算力時代正在全面開啟。 隨著機器學(xué)習(xí)、深度學(xué)習(xí)的快速發(fā)展,人們對高性能服務(wù)器這一概念不再陌生。 伴隨著數(shù)據(jù)分析、數(shù)據(jù)挖掘數(shù)目的不斷增大,傳統(tǒng)的風(fēng)冷散熱方式已經(jīng)不足以滿足散熱需要,這就需要新興的液冷散熱技術(shù)以此滿足節(jié)能減排、靜音高效的需求。
作為國內(nèi)品牌服務(wù)器廠商,藍海大腦液冷GPU服務(wù)器擁有大規(guī)模并行處理能力和無與倫比的靈活性。 它主要用于為計算密集型應(yīng)用程序提供足夠的處理能力。 GPU的優(yōu)勢在于可以由CPU運行應(yīng)用程序代碼,同時圖形處理單元(GPU)可以處理大規(guī)模并行架構(gòu)的計算密集型任務(wù)。 GPU服務(wù)器是遙感測繪、醫(yī)藥研發(fā)、生命科學(xué)和高性能計算的理想選擇。
本文將為大家全面介紹高性能GPU服務(wù)器所涉及技術(shù)以及如何搭建。
線程與線程池
下面將從CPU開始路來到常用的線程池,從底層到上層、從硬件到軟件。
一、CPU
對此大家可能會有疑問,講多線程為什么要從CPU開始? 實際上CPU并沒有線程、進程之類的概念。 CPU所作的就是從內(nèi)存中取出指令——執(zhí)行指令,然后回到1。
1、CPU從哪里取出指令
就是我們熟知的程序計數(shù)器,在這里大家不要把寄存器想的太神秘,可以簡單的將寄存器理解為內(nèi)存,只不過存取速度更快而已。
2、PC寄存器中存放的是什么?
指令(CPU將要執(zhí)行的下一條指令)在內(nèi)存中的地址
3、誰來改變PC寄存器中的指令地址?
由于大部分情況下CPU都是一條接一條順序執(zhí)行,所以
之前PC寄存器中的地址默認是自動加1。 但
當遇到if、else時,這種順序執(zhí)行就被打破了,為了正確的跳轉(zhuǎn)到需要執(zhí)行的指令,CPU在執(zhí)行這類指令時會根據(jù)計算結(jié)果來動態(tài)改變PC寄存器中的值。
4、PC中的初始值是怎么被設(shè)置的?
CPU執(zhí)行的指令來自內(nèi)存,內(nèi)存中的指令來自于磁盤中保存的可執(zhí)行程序加載,磁盤中可執(zhí)行程序是由編譯器生成的,編譯器從定義的函數(shù)生成的機器指令。
二、從CPU到操作系統(tǒng)
從上面我們明白了CPU的工作原理,如果想讓CPU執(zhí)行某個函數(shù),只需把函數(shù)對應(yīng)的第一條機器執(zhí)行裝入PC寄存器就可以了,這樣即使沒有操作系統(tǒng)也可以讓CPU執(zhí)行程序,雖然可行但這是一個非常繁瑣的過程(1、在內(nèi)存中找到一塊大小合適的區(qū)域裝入程序;2、找到函數(shù)入口,設(shè)置好PC寄存器讓CPU開始執(zhí)行程序)。
機器指令由于需加載到內(nèi)存中執(zhí)行所以需要記錄下內(nèi)存的起始地址和長度;同時要找到函數(shù)的入口地址并寫到PC寄存器中。
數(shù)據(jù)結(jié)構(gòu)大致如下:
三、從單核到多核,如何充分利用多核
如果一個程序需要充分利用多核就會遇到以下問題:
1、進程是需要占用內(nèi)存空間的(從上一節(jié)到這一節(jié)),如果多個進程基于同一個可執(zhí)行程序,那么這些進程其內(nèi)存區(qū)域中的內(nèi)容幾乎完全相同,顯然會造成內(nèi)存浪費;
2、當計算機處理的任務(wù)比較復(fù)雜時就會涉及到進程間通信,但是由于各個進程處于不同的內(nèi)存地址空間,而進程間通信需要借助操作系統(tǒng),在增大編程難度的同時也增加了系統(tǒng)開銷。
四、從進程到線程
進程到線程即內(nèi)存中的一段區(qū)域,該區(qū)域保存了CPU執(zhí)行的機器指令以及函數(shù)運行時的堆棧信息。要想讓進程運行,就把main函數(shù)的第一條機器指令地址寫入PC寄存器。
進程的缺點在于只有一個入口函數(shù)(main函數(shù)),進程中的機器指令只能被一個CPU執(zhí)行,那么有沒有辦法讓多個CPU來執(zhí)行同一個進程中的機器指令呢?可以將main函數(shù)的第一條指令地址寫入PC寄存器。
main函數(shù)和
其它函數(shù)沒什么區(qū)別,其特殊之處無非在于是CPU執(zhí)行的第一個函數(shù)。
當把PC寄存器指向非main函數(shù)時,線程就誕生了。
至此一個進程內(nèi)可以有多個入口函數(shù),也就是說屬于同一個進程中的機器指令可以被多個CPU同時執(zhí)行。
多個CPU可以在同一個屋檐下(進程占用的內(nèi)存區(qū)域)同時執(zhí)行屬于該進程的多個入口函數(shù)。操作系統(tǒng)為每個進程維護一堆信息,用來記錄進程所處的內(nèi)存空間等,這堆信息記為數(shù)據(jù)集A。同樣的,操作系統(tǒng)也為線程維護一堆信息,用來記錄線程的入口函數(shù)或者棧信息等,這堆數(shù)據(jù)記為數(shù)據(jù)集B。
顯然數(shù)據(jù)集B要比數(shù)據(jù)A的量要少,由于線程是運行在所處進程的地址空間在程序啟動時已經(jīng)創(chuàng)建完畢,同時線程是程序在運行期間創(chuàng)建的(進程啟動后),所以當線程開始運行的時候這塊地址空間就已經(jīng)存在了,線程可以直接使用。
值得一提的是,有了線程這個概念后,只需要進程開啟后創(chuàng)建多個線程就可以讓所有CPU都忙起來,這就是所謂高性能、高并發(fā)的根本所在。
另外值得注意的一點是:由于各個線程共享進程的內(nèi)存地址空間,所以線程之間的通信無需借助操作系統(tǒng),這給工作人員帶來了便利同時也有不足之處。多線程遇到的多數(shù)問題都出自于線程間通信太方便以至于非常容易出錯。出錯的根源在于CPU執(zhí)行指令時沒有線程的概念,多線程編程面臨的互斥與同步問題需要解決。
最后需要注意的是:雖然前面關(guān)于線程講解使用的圖中用了多個CPU,但并不一定要有多核才能使用多線程,在單核的情況下一樣可以創(chuàng)建出多個線程,主要是由于線程是操作系統(tǒng)層面的實現(xiàn),和有多少個核心是沒有關(guān)系的,CPU在執(zhí)行機器指令時也意識不到執(zhí)行的機器指令屬于哪個線程。即使在只有一個CPU的情況下,操作系統(tǒng)也可以通過線程調(diào)度讓各個線程“同時”向前推進,即將CPU的時間片在各個線程之間來回分配,這樣多個線程看起來就是“同時”運行了,但實際上任意時刻還是只有一個線程在運行。
五、線程與內(nèi)存
前面介紹了線程和CPU的關(guān)系,也就是把CPU的PC寄存器指向線程的入口函數(shù),這樣線程就可以運行起來了。
無論使用任何編程語言,創(chuàng)建一個線程大體相同:
函數(shù)在被執(zhí)行的時產(chǎn)生的數(shù)據(jù)包括:函數(shù)參數(shù)、局部變量、返回地址等信息。這些信息保存在棧中,線程這個概念還沒有出現(xiàn)時進程中只有一個執(zhí)行流,因此只有一個棧,這個棧的棧底就是進程的入口函數(shù),也就是main函數(shù)。
假設(shè)main函數(shù)調(diào)用了funA,funcA又調(diào)用了funcB,如圖所示:
有了線程以后一個進程中就存在多個執(zhí)行入口,即同時存在多個執(zhí)行流,只有一個執(zhí)行流的進程需要一個棧來保存運行時信息,顯然有多個執(zhí)行流時就需要有多個棧來保存各個執(zhí)行流的信息,也就是說操作系統(tǒng)要為每個線程在進程的地址空間中分配一個棧,即每個線程都有獨屬于自己的棧,能意識到這一點是極其關(guān)鍵的。同時創(chuàng)建線程是要消耗進程內(nèi)存空間的。
六、線程的使用
從生命周期的角度講,線程要處理的任務(wù)有兩類:長任務(wù)和短任務(wù)。
1、長任務(wù)(long-lived tasks)
顧名思義,就是任務(wù)存活的時間長。以常用的word為例,在word中編輯的文字需要保存在磁盤上,往磁盤上寫數(shù)據(jù)就是一個任務(wù),這時一個比較好的方法就是專門創(chuàng)建一個寫磁盤的線程,該線程的生命周期和word進程是一樣的,只要打開word就要創(chuàng)建出該線程,當用戶關(guān)閉word時該線程才會被銷毀,這就是長任務(wù)。長任務(wù)非常適合創(chuàng)建專用的線程來處理某些特定任務(wù)。
2、短任務(wù)(short-lived tasks)
即任務(wù)的處理時間短,如一次網(wǎng)絡(luò)請求、一次數(shù)據(jù)庫查詢等。這種任務(wù)可以在短時間內(nèi)快速處理完成。因此短任務(wù)多見于各種Server,像web server、database server、file server、mail server等。該場景有任務(wù)處理所需時間短和任務(wù)數(shù)量巨大的兩個特點。
這種工作方法可對長任務(wù)來說很好,但是對于大量的短任務(wù)來說雖然實現(xiàn)簡單但卻有其缺點:
1)線程是操作系統(tǒng)中的概念,因此創(chuàng)建線程需要借助操作系統(tǒng)來完成,操作系統(tǒng)創(chuàng)建和銷毀線程是需要消耗時間的;
2)每個線程需要有自己獨立的棧,因此當創(chuàng)建大量線程時會消耗過多的內(nèi)存等系統(tǒng)資源。
這就好比一個工廠老板手里有很多訂單,每來一批訂單就要招一批工人,生產(chǎn)的產(chǎn)品非常簡單,工人們很快就能處理完,處理完這批訂單后就把這些工人辭掉,當有新的訂單時再招一遍工人,干活兒5分鐘招人10小時,如果你不是勵志要讓企業(yè)倒閉的話大概是不會這么做到的。因此一個更好的策略就是招一批人后就地養(yǎng)著,有訂單時處理訂單,沒有訂單時大家可以待著。
這就是線程池的由來。
七、從多線程到線程池
線程池的無非就是創(chuàng)建一批線程之后就不再釋放,有任務(wù)就提交給線程處理,因此無需頻繁的創(chuàng)建、銷毀線程,同時由于線程池中的線程個數(shù)通常是固定的,也不會消耗過多的內(nèi)存。
八、線程池是如何工作的?
一般來說提交給線程池的任務(wù)包含需要被處理的數(shù)據(jù)和處理數(shù)據(jù)的函數(shù)兩部分。
偽碼描述一下:
線程池中的線程會阻塞在隊列上,當工作人員向隊列中寫入數(shù)據(jù)后,線程池中的某個線程會被喚醒,該線程從隊列中取出上述結(jié)構(gòu)體(或者對象),以結(jié)構(gòu)體(或者對象)中的數(shù)據(jù)為參數(shù)并調(diào)用處理函數(shù)。
偽碼如下:
八、線程池中線程的數(shù)量
眾所周知線程池的線程過少就不能充分利用CPU,線程創(chuàng)建的過多反而會造成系統(tǒng)性能下降,內(nèi)存占用過多,線程切換造成的消耗等等。因此線程的數(shù)量既不能太多也不能太少,到底該是多少呢?
從處理任務(wù)所需要的資源角度看有CPU密集型和I/O密集型兩種類型。
1、CPU密集型
所謂CPU密集型是指說理任務(wù)不需要依賴外部I/O,比如科學(xué)計算、矩陣運算等。在這種情況下只要線程的數(shù)量和核數(shù)基本相同就可以充分利用CPU資源。
2、I/O密集型
這一類任務(wù)可能計算部分所占用時間不多,大部分時間都用在磁盤I/O、網(wǎng)絡(luò)I/O等方面。
工作人員需要利用性能測試工具評估出用在I/O等待上的時間,這里記為WT(wait time),以及CPU計算所需要的時間,這里記為CT(computing time),那么對于一個N核的系統(tǒng),合適的線程數(shù)大概是 N * (1 + WT/CT) ,假設(shè)I/O等待時間和計算時間相同,那么大概需要2N個線程才能充分利用CPU資源,注意這只是一個理論值,具體設(shè)置多少需要根據(jù)真實的業(yè)務(wù)場景進行測試。
當然充分利用CPU不是唯一需要考慮的點,隨著線程數(shù)量的增多,內(nèi)存占用、系統(tǒng)調(diào)度、打開的文件數(shù)量、打開的socker數(shù)量以及打開的數(shù)據(jù)庫鏈接等等是都需要考慮的。所以沒有萬能公式,要具體情況具體分析。
九、使用線程前需要考慮的因素
1、充分理解任務(wù)是長任務(wù)還是短任務(wù)、是CPU密集型還是I/O密集型,如果兩種都有,那么一種可能更好的辦法是把這兩類任務(wù)放到不同的線程池。
2、如果線程池中的任務(wù)有I/O操作,那么務(wù)必對此任務(wù)設(shè)置超時,否則處理該任務(wù)的線程可能會一直阻塞下去;
4、線程池中的任務(wù)不要同步等待其它任務(wù)的結(jié)果。
I/O與零拷貝技術(shù)
一、什么是I/O?
I/O就是簡單的數(shù)據(jù)Copy,如果數(shù)據(jù)從外部設(shè)備copy到內(nèi)存中就是Input。如果數(shù)據(jù)是內(nèi)存copy到外部設(shè)備則是Output。內(nèi)存與外部設(shè)備之間不嫌麻煩的來回copy數(shù)據(jù)就是Input and Output,簡稱I/O(Input/Output)。
二、I/O與CPU
簡單來說:CPU執(zhí)行機器指令的速度是納秒級別的,而通常的I/O比如磁盤操作,一次磁盤seek大概在毫秒級別,因此如果我們把CPU的速度比作戰(zhàn)斗機的話,那么I/O操作的速度就是肯德雞。
也就是說當程序跑起來時(CPU執(zhí)行機器指令),其速度是要遠遠快于I/O速度。那么接下來的問題就是二者速度相差這么大,該如何設(shè)計、更加合理的高效利用系統(tǒng)資源呢?
既然有速度差異,進程在執(zhí)行完I/O操作前不能繼續(xù)向前推進,那就只有等待(wait)。
三、執(zhí)行I/O時底層都發(fā)生了什么
在支持線程的操作系統(tǒng)中,實際上被調(diào)度的是線程而不是進程,為了更加清晰的理解I/O過程,暫時假設(shè)操作系統(tǒng)只有進程這樣的概念,先不去考慮線程。
如下圖所示,現(xiàn)在內(nèi)存中有兩個進程,進程A和進程B,當前進程A正在運行。如下圖所示:
進程A中有一段讀取文件的代碼,不管在什么語言中通常定義一個用來裝數(shù)據(jù)的buff,然后調(diào)用read之類的函數(shù)。
注意:與CPU執(zhí)行指令的速度相比,I/O操作操作是非常慢的,因此操作系統(tǒng)是不可能把寶貴的CPU計算資源浪費在無謂的等待上的。由于外部設(shè)備執(zhí)行I/O操作是相當慢的,所以在I/O操作完成之前進程是無法繼續(xù)向前推進的,這就是所謂的阻塞,即block。
只需記錄下當前進程的運行狀態(tài)并把CPU的PC寄存器指向其它進程的指令就
操作系統(tǒng)檢測到進程向I/O設(shè)備發(fā)起請求后就暫停進程的運行
。進程有暫停就會有繼續(xù)執(zhí)行,因此操作系統(tǒng)必須保存被暫停的進程以備后續(xù)繼續(xù)執(zhí)行,顯然我們可以用隊列來保存被暫停執(zhí)行的進程。
如上圖所示,操作系統(tǒng)已經(jīng)向磁盤發(fā)送I/O請求,因此磁盤driver開始將磁盤中的數(shù)據(jù)copy到進程A的buff中。雖然這時進程A已經(jīng)被暫停執(zhí)行了,但這并不妨礙磁盤向內(nèi)存中copy數(shù)據(jù)。過程如下圖所示:
操作系統(tǒng)中除了有阻塞隊列之外也有就緒隊列,所謂就緒隊列是指隊列里的進程準備就緒可以被CPU執(zhí)行了。在即使只有1個核的機器上也可以創(chuàng)建出成千上萬個進程,CPU不可能同時執(zhí)行這么多的進程,因此必然存在這樣的進程,即使其一切準備就緒也不能被分配到計算資源,這樣的進程就被放到了就緒隊列。
由于就緒隊列中還有嗷嗷待哺的進程B,所以當進程A被暫停執(zhí)行后CPU是不可以閑下來的。這時操作系統(tǒng)開始在就緒隊列中找下一個可以執(zhí)行的進程,也就是這里的進程B。此時操作系統(tǒng)將進程B從就緒隊列中取出,找出進程B被暫停時執(zhí)行到的機器指令的位置,然后將CPU的PC寄存器指向該位置,這樣進程B就開始運行啦。
如上圖所示,進程B在被CPU執(zhí)行,磁盤在向進程A的內(nèi)存空間中copy數(shù)據(jù),數(shù)據(jù)copy和指令執(zhí)行在同時進行,在操作系統(tǒng)的調(diào)度下,CPU、磁盤都得到了充分的利用。此后磁盤將全部數(shù)據(jù)都copy到了進程A的內(nèi)存中,操作系統(tǒng)接收到磁盤中斷后發(fā)現(xiàn)數(shù)據(jù)copy完畢,進程A重新獲得繼續(xù)運行的資格,操作系統(tǒng)把進程A從阻塞隊列放到了就緒隊列當中。
此后進程B繼續(xù)執(zhí)行,進程A繼續(xù)等待,進程B執(zhí)行了一會兒后操作系統(tǒng)認為進程B執(zhí)行的時間夠長了,因此把進程B放到就緒隊列,把進程A取出并繼續(xù)執(zhí)行。操作系統(tǒng)把進程B放到的是就緒隊列,因此進程B被暫停運行僅僅是因為時間片到了而不是因為發(fā)起I/O請求被阻塞。
四、零拷貝(Zero-copy)
值得注意的一點是:上面的講解中直接把磁盤數(shù)據(jù)copy到了進程空間中,但實際上一般情況下I/O數(shù)據(jù)是要首先copy到操作系統(tǒng)內(nèi)部,然后操作系統(tǒng)再copy到進程空間中。性能要求很高的場景其實也是可以繞過操作系統(tǒng)直接進行數(shù)據(jù)copy,這種繞過操作系統(tǒng)直接進行數(shù)據(jù)copy的技術(shù)被稱為零拷貝(Zero-copy)。
I/O多路復(fù)用
本文我們詳細講解什么是I/O多路復(fù)用以及使用方法,這其中以epoll為代表的I/O多路復(fù)用(基于事件驅(qū)動)技術(shù)使用非常廣泛,實際上你會發(fā)現(xiàn)但凡涉及到高并發(fā)、高性能的場景基本上都能見到事件驅(qū)動的編程方法。
一、什么是文件?
在Linux世界中文件是一個很簡單的概念,只需要將其理解為一個N byte的序列就可以了:
b1, b2, b3, b4, ....... bN
實際上所有的I/O設(shè)備都被抽象了,一切皆文件(Everything is File),磁盤、網(wǎng)絡(luò)數(shù)據(jù)、終端,甚至進程間通信工具管道pipe等都被當做文件對待。
常用的I/O操作接口一般有以下幾類:
1、打開文件,open;
2、改變讀寫位置,seek;
3、文件讀寫,read、write;
4、關(guān)閉文件,close。
二、什么是文件描述符?
在上文中我們講到:要想進行I/O讀操作,像磁盤數(shù)據(jù),需要指定一個buff用來裝入數(shù)據(jù)。在Linux世界要想使用文件,需要借助一個號碼,根據(jù)“弄不懂原則”,這個號碼就被稱為了文件描述符(file descriptors),在Linux世界中鼎鼎大名,其道理和上面那個排隊號碼一樣。文件描述僅僅就是一個數(shù)字而已,但是通過這個數(shù)字我們可以操作一個打開的文件。
有了文件描述符,進程可以對文件一無所知,比如文件在磁盤的什么位置、加載到內(nèi)存中又是怎樣管理的等等,這些信息統(tǒng)統(tǒng)交由操作系統(tǒng)打理,進程無需關(guān)心,操作系統(tǒng)只需要給進程一個文件描述符就足夠了。
三、文件描述符太多了怎么辦?
從上文中我們知道,所有I/O操作都可以通過文件樣的概念來進行,這當然包括網(wǎng)絡(luò)通信。
如果你有一個IM服務(wù)器,當三次握手建議長連接成功以后,我們會調(diào)用accept來獲取一個鏈接,調(diào)用該函數(shù)我們同樣會得到一個文件描述符,通過這個文件描述符就可以處理客戶端發(fā)送的聊天消息并且把消息轉(zhuǎn)發(fā)給接收者。
也就是說,通過這個描述符就可以和客戶端進行通信了:
// 通過accept獲取客戶端的文件描述符
int conn_fd = accept(...);
Server端的處理邏輯通常是接收客戶端消息數(shù)據(jù),然后執(zhí)行轉(zhuǎn)發(fā)(給接收者)邏輯:
if(read(conn_fd, msg_buff) > 0) {
do_transfer(msg_buff);
}
既然主題是高并發(fā),那么Server端就不可能只和一個客戶端通信,而是可能會同時和成千上萬個客戶端進行通信。這時需要處理不再是一個描述符這么簡單,而是有可能要處理成千上萬個描述符。為了不讓問題一上來就過于復(fù)雜先簡單化,假設(shè)只同時處理兩個客戶端的請求。
有的同學(xué)可能會說,這還不簡單,這樣寫不就行了:
if(read(socket_fd1, buff) > 0) { // 處理第一個
do_transfer();
}
if(read(socket_fd2, buff) > 0) { // 處理第二個
do_transfer();
如果此時沒有數(shù)據(jù)可讀那么進程會被阻塞而暫停運行。這時我們就無法處理第二個請求了,即使第二個請求的數(shù)據(jù)已經(jīng)就位,這也就意味著處理某一個客戶端時由于進程被阻塞導(dǎo)致剩下的所有其它客戶端必須等待,在同時處理幾萬客戶端的server上。這顯然是不能容忍的。
聰明的你一定會想到使用多線程:為每個客戶端請求開啟一個線程,這樣一個客戶端被阻塞就不會影響到處理其它客戶端的線程了。注意:既然是高并發(fā),那么我們要為成千上萬個請求開啟成千上萬個線程嗎,大量創(chuàng)建銷毀線程會嚴重影響系統(tǒng)性能。
那么這個問題該怎么解決呢?
這里的關(guān)鍵點在于:我們事先并不知道一個文件描述對應(yīng)的I/O設(shè)備是否是可讀的、是否是可寫的,在外設(shè)的不可讀或不可寫的狀態(tài)下進行I/O只會導(dǎo)致進程阻塞被暫停運行。
三、I/O多路復(fù)用(I/O multiplexing)
multiplexing一詞多用于通信領(lǐng)域,為了充分利用通信線路,希望在一個信道中傳輸多路信號,要想在一個信道中傳輸多路信號就需要把這多路信號結(jié)合為一路,將多路信號組合成一個信號的設(shè)備被稱為Multiplexer(多路復(fù)用器),顯然接收方接收到這一路組合后的信號后要恢復(fù)原先的多路信號,這個設(shè)備被稱為Demultiplexer(多路分用器)。
如下圖所示:
所謂I/O多路復(fù)用指的是這樣一個過程:
1、拿到一堆文件描述符(不管是網(wǎng)絡(luò)相關(guān)的、還是磁盤文件相關(guān)等等,任何文件描述符都可以);
2、通過調(diào)用某個函數(shù)告訴內(nèi)核:“這個函數(shù)你先不要返回,你替我監(jiān)視著這些描述符,當這堆文件描述符中有可以進行I/O讀寫操作的時候你再返回”;
3、當調(diào)用的這個函數(shù)返回后就能知道哪些文件描述符可以進行I/O操作了。
**
三、I/O多路復(fù)用三劍客**
由于調(diào)用這些I/O多路復(fù)用函數(shù)時如果任何一個需要監(jiān)視的文件描述符都不可讀或者可寫那么進程會被阻塞暫停執(zhí)行,直到有文件描述符可讀或者可寫才繼續(xù)運行。所以Linux上的select、poll、epoll都是阻塞式I/O,也就是同步I/O。
1、select:初出茅廬
在select I/O多路復(fù)用機制下,需要把想監(jiān)控的文件描述集合通過函數(shù)參數(shù)的形式告訴select,然后select將這些文件描述符集合拷貝到內(nèi)核中。為了減少這種數(shù)據(jù)拷貝帶來的性能損耗,Linux內(nèi)核對集合的大小做了限制,并規(guī)定用戶監(jiān)控的文件描述集合不能超過1024個,同時當select返回后,僅僅能知道有些文件描述符可以讀寫了。
select的特點
1、能照看的文件描述符數(shù)量有限,不能超過1024個;
2、用戶給文件描述符需要拷貝的內(nèi)核中;
3、只能告訴有文件描述符滿足要求但不知道是哪個。
2、poll:小有所成
poll和select是非常相似,相對于select的優(yōu)化僅僅在于解決文件描述符不能超過1024個的限制,select和poll都會隨著監(jiān)控的文件描述數(shù)量增加而性能下降,因此不適合高并發(fā)場景。
3、epoll:獨步天下
在select面臨的三個問題中,文件描述數(shù)量限制已經(jīng)在poll中解決了,剩下的兩個問題呢?
針對拷貝問題
epoll使用的策略是各個擊破與共享內(nèi)存。文件描述符集合的變化頻率比較低,select和poll頻繁的拷貝整個集合,epoll通過引入epoll_ctl很體貼的做到了只操作那些有變化的文件描述符。同時epoll和內(nèi)核還成為了好朋友,共享了同一塊內(nèi)存,這塊內(nèi)存中保存的就是那些已經(jīng)可讀或者可寫的的文件描述符集合,這樣就減少了內(nèi)核和程序的拷貝開銷。
針對需要遍歷文件描述符才能知道哪個可讀可寫的問題,epoll使用的策略是在select和poll機制下:進程要親自下場去各個文件描述符上等待,任何一個文件描述可讀或者可寫就喚醒進程,但是進程被喚醒后也是一臉懵逼并不知道到底是哪個文件描述符可讀或可寫,還要再從頭到尾檢查一遍。在epoll機制下進程不需要親自下場了,進程只要等待在epoll上,epoll代替進程去各個文件描述符上等待,當哪個文件描述符可讀或者可寫的時候就告訴epoll,由epoll記錄。
在epoll這種機制下,實際上利用的就是“不要打電話給我,有需要我會打給你”這種策略,進程不需要一遍一遍麻煩的問各個文件描述符,而是翻身做主人了——“你們這些文件描述符有哪個可讀或者可寫了主動報上來”。
同步與異步
**
一、同步與異步場景:打電話與發(fā)郵件**
1、同步
通常打電話時都是一個人在說另一個人聽,一個人在說的時候另一個人等待,等另一個人說完后再接著說,因此在這個場景中你可以看到,“依賴”、“關(guān)聯(lián)”、“等待”這些關(guān)鍵詞出現(xiàn)了,因此打電話這種溝通方式就是所謂的同步。
2、異步
另一種常用的溝通方式是郵件,因為沒有人傻等著你寫郵件什么都不做,因此你可以慢慢悠悠的寫,當你在寫郵件時收件人可以去做一些像摸摸魚啊、上個廁所、和同時抱怨一下為什么十一假期不放兩周之類有意義的事情。同時當你寫完郵件發(fā)出去后也不需要干巴巴的等著對方回復(fù)什么都不做,你也可以做一些像摸魚之類這樣有意義的事情。
在這里,你寫郵件別人摸魚,這兩件事又在同時進行,收件人和發(fā)件人都不需要相互等待,發(fā)件人寫完郵件的時候簡單的點個發(fā)送就可以了,收件人收到后就可以閱讀啦,收件人和發(fā)件人不需要相互依賴、不需要相互等待。因此郵件這種溝通方式就是異步的。
二、編程中的同步調(diào)用
一般的函數(shù)調(diào)用都是同步的,就像這樣:
funcA調(diào)用funcB,那么在funcB執(zhí)行完前,funcA中的后續(xù)代碼都不會被執(zhí)行,也就是說funcA必須等待funcB執(zhí)行完成,如下圖所示。
從上圖中可以看出,在funcB運行期間funcA什么都做不了,這就是典型的同步。一般來說,像這種同步調(diào)用,funcA和funcB是運行在同一個線程中的,但值得注意的是即使運行在兩個不能線程中的函數(shù)也可以進行同步調(diào)用,像我們進行IO操作時實際上底層是通過系統(tǒng)調(diào)用的方式向操作系統(tǒng)發(fā)出請求。
如上圖所示,只有當read函數(shù)返回后程序才可以被繼續(xù)執(zhí)行。和上面的同步調(diào)用不同的是,函數(shù)和被調(diào)函數(shù)運行在不同的線程中。由此我們可以得出結(jié)論,同步調(diào)用和函數(shù)與被調(diào)函數(shù)是否運行在同一個線程是沒有關(guān)系的。在這里需要再次強調(diào)同步方式下函數(shù)和被調(diào)函數(shù)無法同時進行。
三、編程中的異步調(diào)用
有同步調(diào)用就有異步調(diào)用。一般來說異步調(diào)用總是和I/O操作等耗時較高的任務(wù)如影隨形,像磁盤文件讀寫、網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)、數(shù)據(jù)庫操作等。
在這里以磁盤文件讀取為例,在read函數(shù)的同步調(diào)用方式下,文件讀取完之前調(diào)用方是無法繼續(xù)向前推進的,但如果read函數(shù)可以異步調(diào)用情況就不一樣了。假如read函數(shù)可以異步調(diào)用的話,即使文件還沒有讀取完成,read函數(shù)也可以立即返回。
如上圖所示,在異步調(diào)用方式下,調(diào)用方不會被阻塞,函數(shù)調(diào)用完成后可以立即執(zhí)行接下來的程序。這時異步的重點在于調(diào)用方接下來的程序執(zhí)行可以和文件讀取同時進行。值得注意的是異步調(diào)用對于程序員來說在理解上是一種負擔,代碼編寫上更是一種負擔,總的來說,上帝在為你打開一扇門的時候會適當?shù)年P(guān)上一扇窗戶。
有的同學(xué)可能會問,在同步調(diào)用下,調(diào)用方不再繼續(xù)執(zhí)行而是暫停等待,被調(diào)函數(shù)執(zhí)行完后很自然的就是調(diào)用方繼續(xù)執(zhí)行,那么異步調(diào)用下調(diào)用方怎知道被調(diào)函數(shù)是否執(zhí)行完成呢?這就分為調(diào)用方根本就不關(guān)心執(zhí)行結(jié)果和調(diào)用方需要知道執(zhí)行結(jié)果兩種情況。
第一種情況比較簡單,無需討論。
第二種情況下就比較有趣了,通常有兩種實現(xiàn)方式:
1、通知機制
當任務(wù)執(zhí)行完成后發(fā)送信號來通知調(diào)用方任務(wù)完成(這里的信號有很多實現(xiàn)方式:Linux中的signal,或使用信號量等機制都可實現(xiàn));
2、回調(diào)機制:
也就是常說的callback。
四、具體的編程例子中理解同步和異步
以常見Web服務(wù)為例來說明這個問題。一般來說Web Server接收到用戶請求后會有一些典型的處理邏輯,最常見的就是數(shù)據(jù)庫查詢(當然,你也可以把這里的數(shù)據(jù)庫查詢換成其它I/O操作,比如磁盤讀取、網(wǎng)絡(luò)通信等),在這里假定處理一次用戶請求需要經(jīng)過步驟A、B、C,然后讀取數(shù)據(jù)庫,數(shù)據(jù)庫讀取完成后需要經(jīng)過步驟D、E、F。
其中步驟A、B、C和D、E、F不需要任何I/O,也就是說這六個步驟不需要讀取文件、網(wǎng)絡(luò)通信等,涉及到I/O操作的只有數(shù)據(jù)庫查詢這一步。一般來說Web Server有主線程和數(shù)據(jù)庫處理線程兩個典型的線程。
首先我們來看下最簡單的實現(xiàn)方式,也就是同步。
這種方式最為自然也最為容易理解:
主線程在發(fā)出數(shù)據(jù)庫查詢請求后就會被阻塞而暫停運行,直到數(shù)據(jù)庫查詢完畢后面的D、E、F才可以繼續(xù)運行,這就是最為典型的同步方法。
如上圖所示,主線程中會有“空隙”,這個空隙就是主線程的“休閑時光”,主線程在這段休閑時光中需要等待數(shù)據(jù)庫查詢完成才能繼續(xù)后續(xù)處理流程。在這里主線程就好比監(jiān)工的老板,數(shù)據(jù)庫線程就好比苦逼搬磚的程序員,在搬完磚前老板什么都不做只是緊緊的盯著你,等你搬完磚后才去忙其它事情。
1、異步情況:主線程不關(guān)心數(shù)據(jù)庫操作結(jié)果
如下圖所示,主線程根本就不關(guān)心數(shù)據(jù)庫是否查詢完畢,數(shù)據(jù)庫查詢完畢后自行處理接下來的D、E、F三個步驟。
一個請求通常需要經(jīng)過七個步驟,其中前三個是在主線程中完成的,后四個是在數(shù)據(jù)庫線程中完成的,數(shù)據(jù)庫線程通過回調(diào)函數(shù)查完數(shù)據(jù)庫后處理D、E、F幾個步驟。
偽碼如下:
主線程處理請求和數(shù)據(jù)庫處理查詢請求可以同時進行,從系統(tǒng)性能上看能更加充分的利用系統(tǒng)資源,更加快速的處理請求;從用戶的角度看,系統(tǒng)的響應(yīng)也會更加迅速。這就是異步的高效之處。但可以看出,異步編程并不如同步來的容易理解,系統(tǒng)可維護性上也不如同步模式。
2、異步情況:主線程關(guān)心數(shù)據(jù)庫操作結(jié)果
如下圖所示,數(shù)據(jù)庫線程需要將查詢結(jié)果利用通知機制發(fā)送給主線程,主線程在接收到消息后繼續(xù)處理上一個請求的后半部分。
由此我們可以看到:ABCDEF幾個步驟全部在主線中處理,同時主線程同樣也沒有了“休閑時光”,只不過在這種情況下數(shù)據(jù)庫線程是比較清閑的,從這里并沒有上一種方法高效,但是依然要比同步模式下要高效。但是要注意的是并不是所有的情況下異步都一定比同步高效,還需要結(jié)合具體業(yè)務(wù)以及IO的復(fù)雜度具體情況具體分析。
高并發(fā)中的協(xié)程
協(xié)程是高性能高并發(fā)編程中不可或缺的技術(shù),包括即時通訊(IM系統(tǒng))在內(nèi)的互聯(lián)網(wǎng)產(chǎn)品應(yīng)用產(chǎn)品中應(yīng)用廣泛,比如號稱支撐微信海量用戶的后臺框架就是基于協(xié)程打造的。而且越來越多的現(xiàn)代編程語言都將協(xié)程視為最重要的語言技術(shù)特征,已知的包括:Go、Python、Kotlin等。
一、從普通函數(shù)到協(xié)程
普通函數(shù)下,只有當執(zhí)行完print("c")這句話后函數(shù)才會返回,但是在協(xié)程下當執(zhí)行完print("a")后func就會因“暫停并返回”這段代碼返回到調(diào)用函數(shù)。
我寫一個return也能返回,就像這樣:
直接寫一個return語句確實也能返回,但這樣寫的話return后面的代碼都不會被執(zhí)行到了。
協(xié)程之所以神奇就神奇在當我們從協(xié)程返回后還能繼續(xù)調(diào)用該協(xié)程,并且是從該協(xié)程的上一個返回點后繼續(xù)執(zhí)行。
就好比孫悟空說一聲“定”,函數(shù)就被暫停了:
這時我們就可以返回到調(diào)用函數(shù),當調(diào)用函數(shù)什么時候想起該協(xié)程后可以再次調(diào)用該協(xié)程,該協(xié)程會從上一個返回點繼續(xù)執(zhí)行。值得注意的是當普通函數(shù)返回后,進程的地址空間中不會再保存該函數(shù)運行時的任何信息,而協(xié)程返回后,函數(shù)的運行時信息是需要保存下來的。
二、“Talk is cheap,show me the code”
在python語言中,這個“定”字同樣使用關(guān)鍵詞yield。
這樣我們的func函數(shù)就變成了:
這時我們的func就不再是簡簡單單的函數(shù)了,而是升級成為了協(xié)程,那么我們該怎么使用呢?
很簡單:
雖然func函數(shù)沒有return語句,也就是說雖然沒有返回任何值,但是我們依然可以寫co = func()這樣的代碼,意思是說co就是拿到的協(xié)程了。
接下來調(diào)用該協(xié)程,使用next(co),運行函數(shù)A看看執(zhí)行到第3行的結(jié)果是什么:
顯然,和預(yù)期一樣協(xié)程func在print("a")后因執(zhí)行yield而暫停并返回函數(shù)A。
接下來是第4行,這個毫無疑問,A函數(shù)在做一些自己的事情,因此會打?。?/p>
接下來是重點的一行,當執(zhí)行第5行再次調(diào)用協(xié)程時該打印什么呢?
如果func是普通函數(shù),那么會執(zhí)行func的第一行代碼,也就是打印a。
但func不是普通函數(shù),而是協(xié)程,我們之前說過,協(xié)程會在上一個返回點繼續(xù)運行,因此這里應(yīng)該執(zhí)行的是func函數(shù)第一個yield之后的代碼,也就是 print("b")。
三、圖形化解釋
為了更加徹底的理解協(xié)程,我們使用圖形化的方式再看一遍。
首先是普通的函數(shù)調(diào)用:
在該圖中方框內(nèi)表示該函數(shù)的指令序列,如果該函數(shù)不調(diào)用任何其它函數(shù),那么應(yīng)該從上到下依次執(zhí)行,但函數(shù)中可以調(diào)用其它函數(shù),因此其執(zhí)行并不是簡單的從上到下,箭頭線表示執(zhí)行流的方向。
從上圖中可以看到:首先來到funcA函數(shù),執(zhí)行一段時間后發(fā)現(xiàn)調(diào)用了另一個函數(shù)funcB,這時控制轉(zhuǎn)移到該函數(shù),執(zhí)行完成后回到main函數(shù)的調(diào)用點繼續(xù)執(zhí)行。這是普通的函數(shù)調(diào)用。
接下來是協(xié)程:
在這里依然首先在funcA函數(shù)中執(zhí)行,運行一段時間后調(diào)用協(xié)程,協(xié)程開始執(zhí)行,直到第一個掛起點,此后就像普通函數(shù)一樣返回funcA函數(shù),funcA函數(shù)執(zhí)行一些代碼后再次調(diào)用該協(xié)程。
三、函數(shù)只是協(xié)程的一種特例
和普通函數(shù)不同的是,協(xié)程能知道自己上一次執(zhí)行到了哪里。協(xié)程會在函數(shù)被暫停運行時保存函數(shù)的運行狀態(tài),并可以從保存的狀態(tài)中恢復(fù)并繼續(xù)運行。
四、協(xié)程的歷史
協(xié)程這種概念早在1958年就已經(jīng)提出來了,要知道這時線程的概念都還沒有提出來。到了1972年,終于有編程語言實現(xiàn)了這個概念,這兩門編程語言就是Simula 67 以及Scheme。但協(xié)程這個概念始終沒有流行起來,甚至在1993年還有人考古一樣專門寫論文挖出協(xié)程這種古老的技術(shù)。
因為這一時期還沒有線程,如果你想在操作系統(tǒng)寫出并發(fā)程序那么你將不得不使用類似協(xié)程這樣的技術(shù),后來線程開始出現(xiàn),操作系統(tǒng)終于開始原生支持程序的并發(fā)執(zhí)行,就這樣,協(xié)程逐漸淡出了程序員的視線。 直到近些年,隨著互聯(lián)網(wǎng)的發(fā)展,尤其是移動互聯(lián)網(wǎng)時代的到來,服務(wù)端對高并發(fā)的要求越來越高,協(xié)程再一次重回技術(shù)主流,各大編程語言都已經(jīng)支持或計劃開始支持協(xié)程。
五、協(xié)程到底如何實現(xiàn)?
讓我們從問題的本質(zhì)出發(fā)來思考這個問題協(xié)程的本質(zhì)是什么呢? 協(xié)程之所以可以被暫停也可以繼續(xù),那么一定要記錄下被暫停時的狀態(tài),也就是上下文,當繼續(xù)運行的時候要恢復(fù)其上下文(狀態(tài))函數(shù)運行時所有的狀態(tài)信息都位于函數(shù)運行時棧中。 如下圖所示,函數(shù)運行時棧就是需要保存的狀態(tài),也就是所謂的上下文。
從上圖中可以看出,該進程中只有一個線程,棧區(qū)中有四個棧幀,main函數(shù)調(diào)用A函數(shù),A函數(shù)調(diào)用B函數(shù),B函數(shù)調(diào)用C函數(shù),當C函數(shù)在運行時整個進程的狀態(tài)就如圖所示。
再仔細想一想,為什么我們要這么麻煩的來回copy數(shù)據(jù)呢? 我們需要做的是直接把協(xié)程的運行需要的棧幀空間直接開辟在堆區(qū)中,這樣都不用來回copy數(shù)據(jù)了,如下圖所示。
從上圖中可以看到該程序中開啟了兩個協(xié)程,這兩個協(xié)程的棧區(qū)都是在堆上分配的,這樣我們就可以隨時中斷或者恢復(fù)協(xié)程的執(zhí)行了。 進程地址空間最上層的棧區(qū)現(xiàn)在的作用是用來保存函數(shù)棧幀的,只不過這些函數(shù)并不是運行在協(xié)程而是普通線程中的。
在上圖中實際上共有一個普通線程和兩個協(xié)程3個執(zhí)行流。 雖然有3個執(zhí)行流但我們創(chuàng)建了幾個線程呢? 答案是:一個線程。
使用協(xié)程理論上我們可以開啟無數(shù)并發(fā)執(zhí)行流,只要堆區(qū)空間足夠,同時還沒有創(chuàng)建線程的開銷,所有協(xié)程的調(diào)度、切換都發(fā)生在用戶態(tài),這就是為什么協(xié)程也被稱作用戶態(tài)線程的原因所在。 所以即使創(chuàng)建了N多協(xié)程,但在操作系統(tǒng)看來依然只有一個線程,也就是說協(xié)程對操作系統(tǒng)來說是不可見的。
這也許是為什么協(xié)程這個概念比線程提出的要早的原因,可能是寫普通應(yīng)用的程序員比寫操作系統(tǒng)的程序員最先遇到需要多個并行流的需求,那時可能都還沒有操作系統(tǒng)的概念,或者操作系統(tǒng)沒有并行這種需求,所以非操作系統(tǒng)程序員只能自己動手實現(xiàn)執(zhí)行流,也就是協(xié)程。
六、協(xié)程技術(shù)概念小結(jié)
1、協(xié)程是比線程更小的執(zhí)行單元
協(xié)程是比線程更小的一種執(zhí)行單元可以認為是輕量級的線程。 之所以說輕的其中一方面的原因是協(xié)程所持有的棧比線程要小很多,java當中會為每個線程分配1M左右的棧空間,而協(xié)程可能只有幾十或者幾百K,棧主要用來保存函數(shù)參數(shù)、局部變量和返回地址等信息。
我們知道而線程的調(diào)度是在操作系統(tǒng)中進行的,而協(xié)程調(diào)度則是在用戶空間進行的,是開發(fā)人員通過調(diào)用系統(tǒng)底層的執(zhí)行上下文相關(guān)api來完成的。 有些語言,比如nodejs、go在語言層面支持了協(xié)程,而有些語言,比如C,需要使用第三方庫才可以擁有協(xié)程的能力。
由于線程是操作系統(tǒng)的最小執(zhí)行單元,因此也可以得出,協(xié)程是基于線程實現(xiàn)的,協(xié)程的創(chuàng)建、切換、銷毀都是在某個線程中來進行的。 使用協(xié)程是因為線程的切換成本比較高,而協(xié)程在這方面很有優(yōu)勢。
2、協(xié)程的切換到底為什么很廉價?
關(guān)于這個問題,回顧一下線程切換的過程:
1)線程在進行切換的時候,需要將CPU中的寄存器的信息存儲起來,然后讀入另外一個線程的數(shù)據(jù),這個會花費一些時間;
2)CPU的高速緩存中的數(shù)據(jù),也可能失效,需要重新加載;
3)線程的切換會涉及到用戶模式到內(nèi)核模式的切換,據(jù)說每次模式切換都需要執(zhí)行上千條指令,很耗時。
實際上協(xié)程的切換之所以快的原因主要是:
1)在切換的時候,寄存器需要保存和加載的數(shù)據(jù)量比較?。?/p>
2)高速緩存可以有效利用;
3)沒有用戶模式到內(nèi)核模式的切換操作;
4)更有效率的調(diào)度,因為協(xié)程是非搶占式的,前一個協(xié)程執(zhí)行完畢或者堵塞,才會讓出CPU,而線程則一般使用了時間片的算法,會進行很多沒有必要的切換。
高性能服務(wù)器到底是如何實現(xiàn)的?
當你在閱讀文章的時候,有沒有想過,服務(wù)器是怎么把這篇文章發(fā)送給你的呢? 說起來很簡單不就是一個用戶請求嗎? 服務(wù)器根據(jù)請求從數(shù)據(jù)庫中撈出這篇文章,然后通過網(wǎng)絡(luò)發(fā)回去嗎。 其實有點復(fù)雜服務(wù)器端到底是如何并行處理成千上萬個用戶請求的呢? 這里面又涉及到哪些技術(shù)呢?
一、多進程
歷史上最早出現(xiàn)也是最簡單的一種并行處理多個請求的方法就是利用多進程。 比如在Linux世界中,可以使用fork、exec等系統(tǒng)調(diào)用創(chuàng)建多個進程,可以在父進程中接收用戶的連接請求,然后創(chuàng)建子進程去處理用戶請求。
1、多進程并行處理的優(yōu)點
1)編程簡單,非常容易理解;
2)由于各個進程的地址空間是相互隔離的,因此一個進程崩潰后并不會影響其它進程;
3)充分利用多核資源。
2、多進程并行處理的缺點
1)各個進程地址空間相互隔離,這一優(yōu)點也會變成缺點,那就是進程間要想通信就會變得比較困難,你需要借助進程間通信機制,想一想你現(xiàn)在知道哪些進程間通信機制,然后讓你用代碼實現(xiàn)呢? 顯然,進程間通信編程相對復(fù)雜,而且性能也是一大問題;
2)創(chuàng)建進程開銷是比線程要大的,頻繁的創(chuàng)建銷毀進程無疑會加重系統(tǒng)負擔。
二、多線程
由于線程共享進程地址空間,因此線程間通信天然不需要借助任何通信機制,直接讀取內(nèi)存就好了。 線程創(chuàng)建銷毀的開銷也變小了,要知道線程就像寄居蟹一樣,房子(地址空間)都是進程的,自己只是一個租客,因此非常的輕量級,創(chuàng)建銷毀的開銷也非常小。
我們可以為每個請求創(chuàng)建一個線程,即使一個線程因執(zhí)行I/O操作——比如讀取數(shù)據(jù)庫等——被阻塞暫停運行也不會影響到其它線程。
由于線程共享進程地址空間,這在為線程間通信帶來便利的同時也帶來了無盡的麻煩。 正是由于線程間共享地址空間,因此一個線程崩潰會導(dǎo)致整個進程崩潰退出,同時線程間通信簡直太簡單了,簡單到線程間通信只需要直接讀取內(nèi)存就可以了,也簡單到出現(xiàn)問題也極其容易,死鎖、線程間的同步互斥、等等,這些極容易產(chǎn)生bug,無數(shù)程序員寶貴的時間就有相當一部分用來解決多線程帶來的無盡問題。
雖然線程也有缺點,但是相比多進程來說,線程更有優(yōu)勢,但想單純的利用多線程就能解決高并發(fā)問題也是不切實際的。因為雖然線程創(chuàng)建開銷相比進程小,但依然也是有開銷的,對于動輒數(shù)萬數(shù)十萬的鏈接的高并發(fā)服務(wù)器來說,創(chuàng)建數(shù)萬個線程會有性能問題,這包括內(nèi)存占用、線程間切換,也就是調(diào)度的開銷。
三、事件驅(qū)動:Event Loop
到目前為止,提到“并行”二字就會想到進程、線程。但是并行編程只能依賴這兩項技術(shù)嗎?并不是這樣的!還有另一項并行技術(shù)廣泛應(yīng)用在GUI編程以及服務(wù)器編程中,這就是近幾年非常流行的事件驅(qū)動編程:event-based concurrency。
大家不要覺得這是一項很難懂的技術(shù),實際上事件驅(qū)動編程原理上非常簡單。
這一技術(shù)需要兩種原料:
1)event;
2)處理event的函數(shù),這一函數(shù)通常被稱為event handler;
由于對于網(wǎng)絡(luò)通信服務(wù)器來說,處理一個用戶請求時大部分時間其實都用在了I/O操作上,像數(shù)據(jù)庫讀寫、文件讀寫、網(wǎng)絡(luò)讀寫等。當一個請求到來,簡單處理之后可能就需要查詢數(shù)據(jù)庫等I/O操作,我們知道I/O是非常慢的,當發(fā)起I/O后我們大可以不用等待該I/O操作完成就可以繼續(xù)處理接下來的用戶請求。所以一個event loop可以同時處理多個請求。
四、事件來源:IO多路復(fù)用
IO多路復(fù)用技術(shù)通過一次監(jiān)控多個文件描述,當某個“文件”(實際可能是im網(wǎng)絡(luò)通信中socket)可讀或者可寫的時候我們就能同時處理多個文件描述符啦。
這樣IO多路復(fù)用技術(shù)就成了event loop的原材料供應(yīng)商,源源不斷的給我們提供各種event,這樣關(guān)于event來源的問題就解決了。
五、問題:阻塞式IO
當我們進行IO操作,比如讀取文件時,如果文件沒有讀取完成,那么我們的程序(線程)會被阻塞而暫停執(zhí)行,這在多線程中不是問題,因為操作系統(tǒng)還可以調(diào)度其它線程。 但是在單線程的event loop中是有問題的,原因就在于當我們在event loop中執(zhí)行阻塞式IO操作時整個線程(event loop)會被暫停運行,這時操作系統(tǒng)將沒有其它線程可以調(diào)度,因為系統(tǒng)中只有一個event loop在處理用戶請求,這樣當event loop線程被阻塞暫停運行時所有用戶請求都沒有辦法被處理。 你能想象當服務(wù)器在處理其它用戶請求讀取數(shù)據(jù)庫導(dǎo)致你的請求被暫停嗎?
因此:在基于事件驅(qū)動編程時有一條注意事項,那就是不允許發(fā)起阻塞式IO。 有的同學(xué)可能會問,如果不能發(fā)起阻塞式IO的話,那么該怎樣進行IO操作呢?
六、解決方法:非阻塞式IO
為克服阻塞式IO所帶來的問題,現(xiàn)代操作系統(tǒng)開始提供一種新的發(fā)起IO請求的方法,這種方法就是異步IO。 對應(yīng)的,阻塞式IO就是同步IO,關(guān)于同步和異步詳見上文。
異步IO時,假設(shè)調(diào)用aio_read函數(shù)(具體的異步IO API請參考具體的操作系統(tǒng)平臺),也就是異步讀取,當我們調(diào)用該函數(shù)后可以立即返回,并繼續(xù)其它事情,雖然此時該文件可能還沒有被讀取,這樣就不會阻塞調(diào)用線程了。 此外,操作系統(tǒng)還會提供其它方法供調(diào)用線程來檢測IO操作是否完成。
七、基于事件驅(qū)動并行編程的難點
雖然有異步IO來解決event loop可能被阻塞的問題,但是基于事件編程依然是困難的。
首先event loop是運行在一個線程中的,顯然一個線程是沒有辦法充分利用多核資源的,有的同學(xué)可能會說那就創(chuàng)建多個event loop實例不就可以了,這樣就有多個event loop線程了,但是這樣一來多線程問題又會出現(xiàn)。
其次在于編程方面,異步編程需要結(jié)合回調(diào)函數(shù)(這種編程方式需要把處理邏輯分為兩部分:一部分調(diào)用方自己處理,另一部分在回調(diào)函數(shù)中處理),這一編程方式的改變加重了程序員在理解上的負擔,基于事件編程的項目后期會很難擴展以及維護。
八、更好的方法
有沒有一種方法既能結(jié)合同步IO的簡單理解又不會因同步調(diào)用導(dǎo)致線程被阻塞呢? 答案是肯定的,這就是用戶態(tài)線程(user level thread),也就是大名鼎鼎的協(xié)程。
雖然基于事件編程有這樣那樣的缺點,但是在當今的高性能高并發(fā)服務(wù)器上基于事件編程方式依然非常流行,但已經(jīng)不是純粹的基于單一線程的事件驅(qū)動了,而是 event loop + multi thread + user level thread。
進程、線程、協(xié)程
一、什么是進程?
1、基本常識
計算機的核心是CPU,它承擔了所有的計算任務(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)一的標準定義。
進程一般由程序、數(shù)據(jù)集合和進程控制塊三部分組成:
程序用于描述進程要完成的功能,是控制進程執(zhí)行的指令集;
數(shù)據(jù)集合是程序在執(zhí)行時所需要的數(shù)據(jù)和工作區(qū);
程序控制塊(Program Control Block,簡稱PCB),包含進程的描述信息和控制信息,是進程存在的唯一標志。
進程的特點:
動態(tài)性:進程是程序的一次執(zhí)行過程,是臨時的,有生命期的,是動態(tài)產(chǎn)生,動態(tài)消亡的;
并發(fā)性:任何進程都可以同其他進程一起并發(fā)執(zhí)行;
獨立性:進程是系統(tǒng)進行資源分配和調(diào)度的一個獨立單位;
結(jié)構(gòu)性:進程由程序、數(shù)據(jù)和進程控制塊三部分組成。
2、為什么要有多進程?
多進程目的是提高cpu的使用率。 假設(shè)只有一個進程(先不談多線程),從操作系統(tǒng)的層面看,我們使用打印機的步驟有如下:
1)使用CPU執(zhí)行程序,去硬盤讀取需要打印的文件,然后CPU會長時間的等待,直到硬盤讀寫完成;
2)使用CPU執(zhí)行程序,讓打印機打印這些內(nèi)容,然后CPU會長時間的等待,等待打印結(jié)束。
在這樣的情況下:其實CPU的使用率其實非常的低。
打印一個文件從頭到尾需要的時間可能是1分鐘,而cpu使用的時間總和可能加起來只有幾秒鐘。 而后面如果單進程執(zhí)行游戲的程序的時候,CPU也同樣會有大量的空閑時間。
使用多進程后:
當CPU在等待硬盤讀寫文件,或者在等待打印機打印的時候,CPU可以去執(zhí)行游戲的程序,這樣CPU就能盡可能高的提高使用率。
再具體一點說,其實也提高了效率。 因為在等待打印機的時候,這時候顯卡也是閑置的,如果用多進程并行的話,游戲進程完全可以并行使用顯卡,并且與打印機之間也不會互相影響。
3、總結(jié)
進程直觀點說是保存在硬盤上的程序運行以后,會在內(nèi)存空間里形成一個獨立的內(nèi)存體,這個內(nèi)存體有自己獨立的地址空間,有自己的堆,上級掛靠單位是操作系統(tǒng)。 操作系統(tǒng)會進程為單位,分配系統(tǒng)資源(CPU時間片、內(nèi)存等資源),進程是資源分配的最小單位。
二、什么是線程?
1、基本常識
早期操作系統(tǒng)中并沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執(zhí)行的最小單位。 任務(wù)調(diào)度采用的是時間片輪轉(zhuǎn)的搶占式調(diào)度方式,而進程是任務(wù)調(diào)度的最小單位,每個進程有各自獨立的一塊內(nèi)存,使得各個進程之間內(nèi)存地址相互隔離。 后來隨著計算機的發(fā)展,對CPU的要求越來越高,進程之間的切換開銷較大,已經(jīng)無法滿足越來越復(fù)雜的程序的要求了。 于是就發(fā)明了線程。
線程是程序執(zhí)行中一個單一的順序控制流程:
1)程序執(zhí)行流的最小單元
2)處理器調(diào)度和分派的基本單位
一個進程可以有一個或多個線程,各個線程之間共享程序的內(nèi)存空間(也就是所在進程的內(nèi)存空間)。 一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成。 而進程由內(nèi)存空間(代碼、數(shù)據(jù)、進程空間、打開的文件)和一個或多個線程組成。
如上圖所示,在任務(wù)管理器的進程一欄里,有道詞典和有道云筆記就是進程,而在進程下又有著多個執(zhí)行不同任務(wù)的線程。
2、任務(wù)調(diào)度
線程是什么? 要理解這個概念,需要先了解一下操作系統(tǒng)的一些相關(guān)概念。 大部分操作系統(tǒng)(如Windows、Linux)的任務(wù)調(diào)度是采用時間片輪轉(zhuǎn)的搶占式調(diào)度方式。 在一個進程中:當一個線程任務(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í)行)。
3、進程與線程的區(qū)別
進程與線程的關(guān)系
1)線程是程序執(zhí)行的最小單位,而進程是操作系統(tǒng)分配資源的最小單位;
2)一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執(zhí)行路線;
3)進程之間相互獨立,但同一進程下的各個線程之間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)及一些進程級的資源(如打開文件和信號),某進程內(nèi)的線程在其它進程不可見;
4)線程上下文切換比進程上下文切換要快得多。
▲ 進程與線程的資源共享關(guān)系
▲ 單線程與多線程的關(guān)系
總之線程和進程都是一種抽象的概念,線程是一種比進程更小的抽象,線程和進程都可用于實現(xiàn)并發(fā)。
在早期的操作系統(tǒng)中并沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執(zhí)行的最小單位。 它相當于一個進程里只有一個線程,進程本身就是線程。 所以線程有時被稱為輕量級進程。
后來隨著計算機的發(fā)展,對多個任務(wù)之間上下文切換的效率要求越來越高,就抽象出一個更小的概念——線程,一般一個進程會有多個(也可以是一個)線程。
4、多線程與多核
上面提到的時間片輪轉(zhuǎn)的調(diào)度方式說一個任務(wù)執(zhí)行一小段時間后強制暫停去執(zhí)行下一個任務(wù),每個任務(wù)輪流執(zhí)行。很多操作系統(tǒng)的書都說“同一時間點只有一個任務(wù)在執(zhí)行”。其實“同一時間點只有一個任務(wù)在執(zhí)行”這句話是不準確的,至少它是不全面的。那多核處理器的情況下,線程是怎樣執(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)系有三種模型:
1)一對一模型;
2)多對一模型;
3)多對多模型。
5、一對一模型
對于一對一模型來說:一個用戶線程就唯一地對應(yīng)一個內(nèi)核線程(反過來不一定成立,一個內(nèi)核線程不一定有對應(yīng)的用戶線程)。 這樣如果CPU沒有采用超線程技術(shù)(如四核四線程的計算機),一個用戶線程就唯一地映射到一個物理CPU的內(nèi)核線程,線程之間的并發(fā)是真正的并發(fā)。
一對一模型優(yōu)點
使用戶線程具有與內(nèi)核線程一樣的優(yōu)點一個線程因某種原因阻塞時其他線程的執(zhí)行不受影響(此處,一對一模型也可以讓多線程程序在多處理器的系統(tǒng)上有更好的表現(xiàn))。
一對一模型缺點
1)許多操作系統(tǒng)限制了內(nèi)核線程的數(shù)量,因此一對一模型會使用戶線程的數(shù)量受到限制;
2)許多操作系統(tǒng)內(nèi)核線程調(diào)度時,上下文切換的開銷較大,導(dǎo)致用戶線程的執(zhí)行效率下降。
▲ 一對一模型
6、多對一模型
多對一模型將多個用戶線程映射到一個內(nèi)核線程上,線程之間的切換由用戶態(tài)的代碼來進行,系統(tǒng)內(nèi)核感受不到線程的實現(xiàn)方式。 用戶線程的建立、同步、銷毀等都在用戶態(tài)中完成,不需要內(nèi)核的介入。
多對一模型優(yōu)點
1)多對一模型的線程上下文切換速度要快許多;
2)多對一模型對用戶線程的數(shù)量幾乎無限制。
多對一模型缺點
1)如果其中一個用戶線程阻塞,那么其它所有線程都將無法執(zhí)行,因為此時內(nèi)核線程也隨之阻塞了;
2)在多處理器系統(tǒng)上,處理器數(shù)量的增加對多對一模型的線程性能不會有明顯的增加,因為所有的用戶線程都映射到一個處理器上了。
▲ 多對一模型
7、多對多模型
多對多模型結(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)點
1)一個用戶線程的阻塞不會導(dǎo)致所有線程的阻塞,因為此時還有別的內(nèi)核線程被調(diào)度來執(zhí)行;
2)多對多模型對用戶線程的數(shù)量沒有限制;
3)在多處理器的操作系統(tǒng)中,多對多模型的線程也能得到一定的性能提升,但提升的幅度不如一對一模型的高。
▲ 多對多模型
在現(xiàn)在流行的操作系統(tǒng)中,大都采用多對多的模型。
8、查看進程與線程
一個應(yīng)用程序可能是多線程的,也可能是多進程的,如何查看呢?
在Windows下我們只須打開任務(wù)管理器就能查看一個應(yīng)用程序的進程和線程數(shù)。 按“Ctrl+Alt+Del”或右鍵快捷工具欄打開任務(wù)管理器。
在“進程”選項卡下,我們可以看到一個應(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)視器。
▲ 查看CPU和內(nèi)存的使用率
9、線程的生命周期
當線程的數(shù)量小于處理器的數(shù)量時,線程的并發(fā)是真正的并發(fā),不同的線程運行在不同的處理器上。 但當線程的數(shù)量大于處理器的數(shù)量時,線程的并發(fā)會受到一些阻礙,此時并不是真正的并發(fā),因為此時至少有一個處理器會運行多個線程。
在單個處理器運行多個線程時,并發(fā)是一種模擬出來的狀態(tài)。 操作系統(tǒng)采用時間片輪轉(zhuǎn)的方式輪流執(zhí)行每一個線程。 現(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)建、就緒、運行、阻塞(等待)、退出。 早期的進程相當于現(xiàn)在的只有單個線程的進程,那么現(xiàn)在的多線程也有五種狀態(tài),現(xiàn)在的多線程的生命周期與早期進程的生命周期類似。
▲ 早期進程的生命周期
進程在運行過程有三種狀態(tài):就緒、運行、阻塞,創(chuàng)建和退出狀態(tài)描述的是進程的創(chuàng)建過程和退出過程。
早期進程的生命周期:
創(chuàng)建:進程正在創(chuàng)建,還不能運行。操作系統(tǒng)在創(chuàng)建進程時要進行的工作包括分配和建立進程控制塊表項、建立資源表格并分配資源、加載程序并建立地址空間;
就緒:時間片已用完,此線程被強制暫停,等待下一個屬于它的時間片到來;
運行:此線程正在執(zhí)行,正在占用時間片;
阻塞:也叫等待狀態(tài),等待某一事件(如IO或另一個線程)執(zhí)行完;
退出:進程已結(jié)束,所以也稱結(jié)束狀態(tài),釋放操作系統(tǒng)分配的資源。
▲ 線程的生命周期
線程的生命周期跟進程很類似:
創(chuàng)建:一個新的線程被創(chuàng)建,等待該線程被調(diào)用執(zhí)行;
就緒:時間片已用完,此線程被強制暫停,等待下一個屬于它的時間片到來;
運行:此線程正在執(zhí)行,正在占用時間片;
阻塞:也叫等待狀態(tài),等待某一事件(如IO或另一個線程)執(zhí)行完;
退出:一個線程完成任務(wù)或者其他終止條件發(fā)生,該線程終止進入退出狀態(tài),退出狀態(tài)釋放該線程所分配的資源。
五、什么是協(xié)程?
1、基本常識
協(xié)程是一種基于線程之上,但又比線程更加輕量級的存在,這種由程序員自己寫程序來管理的輕量級線程叫做“用戶空間線程”,具有對內(nèi)核來說不可見的特性。由于是自主開辟的異步任務(wù),所以很多人也更喜歡叫它們纖程(Fiber),或者綠色線程(GreenThread)。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協(xié)程。
2、協(xié)程的目的
對于Java程序員來說,在傳統(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é)程的目的就是當出現(xiàn)長時間的I/O操作時,通過讓出目前的協(xié)程調(diào)度,執(zhí)行下一個任務(wù)的方式,來消除ContextSwitch上的開銷。
3、協(xié)程的特點
協(xié)程的特點總結(jié)一下就是:
1)線程的切換由操作系統(tǒng)負責(zé)調(diào)度,協(xié)程由用戶自己進行調(diào)度,因此減少了上下文切換,提高了效率;
2)線程的默認Stack大小是1M,而協(xié)程更輕量,接近1K。 因此可以在相同的內(nèi)存中開啟更多的協(xié)程;
3)由于在同一個線程上,因此可以避免競爭關(guān)系而使用鎖;
4)適用于被阻塞的,且需要大量并發(fā)的場景。 但不適用于大量計算的多線程,遇到此種情況,更好實用線程去解決。
4、協(xié)程的原理
當出現(xiàn)IO阻塞的時候,由協(xié)程的調(diào)度器進行調(diào)度,通過將數(shù)據(jù)流立刻yield掉(主動讓出),并且記錄當前棧上的數(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上的開銷。
5、協(xié)程和線程的比較
六、總結(jié)
1、進程和線程的區(qū)別
1)調(diào)度:線程作為調(diào)度和分配的基本單位,進程作為擁有資源的基本單位;
2)并發(fā)性:不僅進程之間可以并發(fā)執(zhí)行,同一個進程的多個線程之間也可并發(fā)執(zhí)行;
3)擁有資源:進程是擁有資源的一個獨立單位,線程不擁有系統(tǒng)資源,但可以訪問隸屬于進程的資源;
4)系統(tǒng)開銷:在創(chuàng)建或撤消進程時,由于系統(tǒng)都要為之分配和回收資源,導(dǎo)致系統(tǒng)的開銷明顯大于創(chuàng)建或撤消線程時的開銷。
2、進程和線程的聯(lián)系
1)一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程;
2)資源分配給進程,同一進程的所有線程共享該進程的所有資源;
3)處理機分給線程,即真正在處理機上運行的是線程;
4)線程在執(zhí)行過程中,需要協(xié)作同步。 不同進程的線程間要利用消息通信的辦法實現(xiàn)同步。
開發(fā)者在每個線程中只做非常輕量的操作,比如訪問一個極小的文件,下載一張極小的圖片,加載一段極小的文本等。 但是,這樣”輕量的操作“的量卻非常多。
在有大量這樣的輕量操作的場景下,即使可以通過使用線程池來避免創(chuàng)建與銷毀的開銷,但是線程切換的開銷也會非常大,甚至于接近操作本身的開銷。 對于這些場景,就非常需要一種可以減少這些開銷的方式。 于是,協(xié)程就應(yīng)景而出,非常適合這樣的場景。
審核編輯:湯梓紅
-
gpu
+關(guān)注
關(guān)注
28文章
4742瀏覽量
128972 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
9184瀏覽量
85490 -
線程
+關(guān)注
關(guān)注
0文章
505瀏覽量
19695 -
進程
+關(guān)注
關(guān)注
0文章
203瀏覽量
13962
發(fā)布評論請先 登錄
相關(guān)推薦
評論