女人自慰AV免费观看内涵网,日韩国产剧情在线观看网址,神马电影网特片网,最新一级电影欧美,在线观看亚洲欧美日韩,黄色视频在线播放免费观看,ABO涨奶期羡澄,第一导航fulione,美女主播操b

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

如何理解高性能服務器的高性能、高并發?

GPU視覺識別 ? 來源:GPU視覺識別 ? 作者:GPU視覺識別 ? 2023-01-13 09:45 ? 次閱讀

線程 | 同步 | 異步 | 異構

協程 | 進程 | 同構 | 線程池

當前,隨著“東數西算”政策的落地,算力時代正在全面開啟。 隨著機器學習深度學習的快速發展,人們對高性能服務器這一概念不再陌生。 伴隨著數據分析、數據挖掘數目的不斷增大,傳統的風冷散熱方式已經不足以滿足散熱需要,這就需要新興的液冷散熱技術以此滿足節能減排、靜音高效的需求。

作為國內品牌服務器廠商,藍海大腦液冷GPU服務器擁有大規模并行處理能力和無與倫比的靈活性。 它主要用于為計算密集型應用程序提供足夠的處理能力。 GPU的優勢在于可以由CPU運行應用程序代碼,同時圖形處理單元(GPU)可以處理大規模并行架構的計算密集型任務。 GPU服務器是遙感測繪、醫藥研發、生命科學和高性能計算的理想選擇。

本文將為大家全面介紹高性能GPU服務器所涉及技術以及如何搭建。

線程與線程池

下面將從CPU開始路來到常用的線程池,從底層到上層、從硬件到軟件。

一、CPU

對此大家可能會有疑問,講多線程為什么要從CPU開始? 實際上CPU并沒有線程、進程之類的概念。 CPU所作的就是從內存中取出指令——執行指令,然后回到1。

poYBAGPAt4GAUDnrAACRrBWX_Co680.png

1、CPU從哪里取出指令

就是我們熟知的程序計數器,在這里大家不要把寄存器想的太神秘,可以簡單的將寄存器理解為內存,只不過存取速度更快而已。

2、PC寄存器中存放的是什么?

指令(CPU將要執行的下一條指令)在內存中的地址

poYBAGPAt4GAR3tiAABLKqJvkiw928.png

3、誰來改變PC寄存器中的指令地址?

由于大部分情況下CPU都是一條接一條順序執行,所以

之前PC寄存器中的地址默認是自動加1。 但

當遇到if、else時,這種順序執行就被打破了,為了正確的跳轉到需要執行的指令,CPU在執行這類指令時會根據計算結果來動態改變PC寄存器中的值。

4、PC中的初始值是怎么被設置的?

CPU執行的指令來自內存,內存中的指令來自于磁盤中保存的可執行程序加載,磁盤中可執行程序是由編譯器生成的,編譯器從定義的函數生成的機器指令。

pYYBAGPAt4KANadxAABj2ZjEv_k505.png

二、從CPU到操作系統

從上面我們明白了CPU的工作原理,如果想讓CPU執行某個函數,只需把函數對應的第一條機器執行裝入PC寄存器就可以了,這樣即使沒有操作系統也可以讓CPU執行程序,雖然可行但這是一個非常繁瑣的過程(1、在內存中找到一塊大小合適的區域裝入程序;2、找到函數入口,設置好PC寄存器讓CPU開始執行程序)。

機器指令由于需加載到內存中執行所以需要記錄下內存的起始地址和長度;同時要找到函數的入口地址并寫到PC寄存器中。

數據結構大致如下:

三、從單核到多核,如何充分利用多核

如果一個程序需要充分利用多核就會遇到以下問題:

1、進程是需要占用內存空間的(從上一節到這一節),如果多個進程基于同一個可執行程序,那么這些進程其內存區域中的內容幾乎完全相同,顯然會造成內存浪費;

2、當計算機處理的任務比較復雜時就會涉及到進程間通信,但是由于各個進程處于不同的內存地址空間,而進程間通信需要借助操作系統,在增大編程難度的同時也增加了系統開銷。

四、從進程到線程

進程到線程即內存中的一段區域,該區域保存了CPU執行的機器指令以及函數運行時的堆棧信息。要想讓進程運行,就把main函數的第一條機器指令地址寫入PC寄存器。

poYBAGPAt4KANmvDAAApaOgbIHI774.png

進程的缺點在于只有一個入口函數(main函數),進程中的機器指令只能被一個CPU執行,那么有沒有辦法讓多個CPU來執行同一個進程中的機器指令呢?可以將main函數的第一條指令地址寫入PC寄存器。

main函數和

其它函數沒什么區別,其特殊之處無非在于是CPU執行的第一個函數。

當把PC寄存器指向非main函數時,線程就誕生了。

pYYBAGPAt4KAXyY2AAA9ftxZG-c511.png

至此一個進程內可以有多個入口函數,也就是說屬于同一個進程中的機器指令可以被多個CPU同時執行。

poYBAGPAt4KANPekAAAmW335fTU841.png

多個CPU可以在同一個屋檐下(進程占用的內存區域)同時執行屬于該進程的多個入口函數。操作系統為每個進程維護一堆信息,用來記錄進程所處的內存空間等,這堆信息記為數據集A。同樣的,操作系統也為線程維護一堆信息,用來記錄線程的入口函數或者棧信息等,這堆數據記為數據集B。

顯然數據集B要比數據A的量要少,由于線程是運行在所處進程的地址空間在程序啟動時已經創建完畢,同時線程是程序在運行期間創建的(進程啟動后),所以當線程開始運行的時候這塊地址空間就已經存在了,線程可以直接使用。

值得一提的是,有了線程這個概念后,只需要進程開啟后創建多個線程就可以讓所有CPU都忙起來,這就是所謂高性能、高并發的根本所在。

poYBAGPAt4KANAfpAAA-QZvcbUs016.png

另外值得注意的一點是:由于各個線程共享進程的內存地址空間,所以線程之間的通信無需借助操作系統,這給工作人員帶來了便利同時也有不足之處。多線程遇到的多數問題都出自于線程間通信太方便以至于非常容易出錯。出錯的根源在于CPU執行指令時沒有線程的概念,多線程編程面臨的互斥與同步問題需要解決。

最后需要注意的是:雖然前面關于線程講解使用的圖中用了多個CPU,但并不一定要有多核才能使用多線程,在單核的情況下一樣可以創建出多個線程,主要是由于線程是操作系統層面的實現,和有多少個核心是沒有關系的,CPU在執行機器指令時也意識不到執行的機器指令屬于哪個線程。即使在只有一個CPU的情況下,操作系統也可以通過線程調度讓各個線程“同時”向前推進,即將CPU的時間片在各個線程之間來回分配,這樣多個線程看起來就是“同時”運行了,但實際上任意時刻還是只有一個線程在運行。

五、線程與內存

前面介紹了線程和CPU的關系,也就是把CPU的PC寄存器指向線程的入口函數,這樣線程就可以運行起來了。

無論使用任何編程語言,創建一個線程大體相同:

函數在被執行的時產生的數據包括:函數參數、局部變量、返回地址等信息。這些信息保存在棧中,線程這個概念還沒有出現時進程中只有一個執行流,因此只有一個棧,這個棧的棧底就是進程的入口函數,也就是main函數。

假設main函數調用了funA,funcA又調用了funcB,如圖所示:

poYBAGPAt4KAce0OAAAzN8fnnuI762.png

有了線程以后一個進程中就存在多個執行入口,即同時存在多個執行流,只有一個執行流的進程需要一個棧來保存運行時信息,顯然有多個執行流時就需要有多個棧來保存各個執行流的信息,也就是說操作系統要為每個線程在進程的地址空間中分配一個棧,即每個線程都有獨屬于自己的棧,能意識到這一點是極其關鍵的。同時創建線程是要消耗進程內存空間的。

六、線程的使用

從生命周期的角度講,線程要處理的任務有兩類:長任務和短任務。

1、長任務(long-lived tasks)

顧名思義,就是任務存活的時間長。以常用的word為例,在word中編輯的文字需要保存在磁盤上,往磁盤上寫數據就是一個任務,這時一個比較好的方法就是專門創建一個寫磁盤的線程,該線程的生命周期和word進程是一樣的,只要打開word就要創建出該線程,當用戶關閉word時該線程才會被銷毀,這就是長任務。長任務非常適合創建專用的線程來處理某些特定任務。

2、短任務(short-lived tasks)

即任務的處理時間短,如一次網絡請求、一次數據庫查詢等。這種任務可以在短時間內快速處理完成。因此短任務多見于各種Server,像web server、database server、file server、mail server等。該場景有任務處理所需時間短和任務數量巨大的兩個特點。

poYBAGPAt4KAIuMnAABmYd0A0kk263.png

這種工作方法可對長任務來說很好,但是對于大量的短任務來說雖然實現簡單但卻有其缺點:

1)線程是操作系統中的概念,因此創建線程需要借助操作系統來完成,操作系統創建和銷毀線程是需要消耗時間的;

2)每個線程需要有自己獨立的棧,因此當創建大量線程時會消耗過多的內存等系統資源。

這就好比一個工廠老板手里有很多訂單,每來一批訂單就要招一批工人,生產的產品非常簡單,工人們很快就能處理完,處理完這批訂單后就把這些工人辭掉,當有新的訂單時再招一遍工人,干活兒5分鐘招人10小時,如果你不是勵志要讓企業倒閉的話大概是不會這么做到的。因此一個更好的策略就是招一批人后就地養著,有訂單時處理訂單,沒有訂單時大家可以待著。

這就是線程池的由來。

七、從多線程到線程池

線程池的無非就是創建一批線程之后就不再釋放,有任務就提交給線程處理,因此無需頻繁的創建、銷毀線程,同時由于線程池中的線程個數通常是固定的,也不會消耗過多的內存。

八、線程池是如何工作的?

一般來說提交給線程池的任務包含需要被處理的數據和處理數據的函數兩部分。

偽碼描述一下:

線程池中的線程會阻塞在隊列上,當工作人員向隊列中寫入數據后,線程池中的某個線程會被喚醒,該線程從隊列中取出上述結構體(或者對象),以結構體(或者對象)中的數據為參數并調用處理函數。

偽碼如下:

八、線程池中線程的數量

眾所周知線程池的線程過少就不能充分利用CPU,線程創建的過多反而會造成系統性能下降,內存占用過多,線程切換造成的消耗等等。因此線程的數量既不能太多也不能太少,到底該是多少呢?

從處理任務所需要的資源角度看有CPU密集型和I/O密集型兩種類型。

1、CPU密集型

所謂CPU密集型是指說理任務不需要依賴外部I/O,比如科學計算、矩陣運算等。在這種情況下只要線程的數量和核數基本相同就可以充分利用CPU資源。

poYBAGPAt4KABNESAAAgn6krlkg126.png

2、I/O密集型

這一類任務可能計算部分所占用時間不多,大部分時間都用在磁盤I/O、網絡I/O等方面。

pYYBAGPAt4GAR8rnAAAg52fICik201.png

工作人員需要利用性能測試工具評估出用在I/O等待上的時間,這里記為WT(wait time),以及CPU計算所需要的時間,這里記為CT(computing time),那么對于一個N核的系統,合適的線程數大概是 N * (1 + WT/CT) ,假設I/O等待時間和計算時間相同,那么大概需要2N個線程才能充分利用CPU資源,注意這只是一個理論值,具體設置多少需要根據真實的業務場景進行測試。

當然充分利用CPU不是唯一需要考慮的點,隨著線程數量的增多,內存占用、系統調度、打開的文件數量、打開的socker數量以及打開的數據庫鏈接等等是都需要考慮的。所以沒有萬能公式,要具體情況具體分析。

九、使用線程前需要考慮的因素

1、充分理解任務是長任務還是短任務、是CPU密集型還是I/O密集型,如果兩種都有,那么一種可能更好的辦法是把這兩類任務放到不同的線程池。

2、如果線程池中的任務有I/O操作,那么務必對此任務設置超時,否則處理該任務的線程可能會一直阻塞下去;

4、線程池中的任務不要同步等待其它任務的結果。

I/O與零拷貝技術

一、什么是I/O?

I/O就是簡單的數據Copy,如果數據從外部設備copy到內存中就是Input。如果數據是內存copy到外部設備則是Output。內存與外部設備之間不嫌麻煩的來回copy數據就是Input and Output,簡稱I/O(Input/Output)。

poYBAGPAt4KALO_WAACfWnaawiY849.jpg

二、I/O與CPU

簡單來說:CPU執行機器指令的速度是納秒級別的,而通常的I/O比如磁盤操作,一次磁盤seek大概在毫秒級別,因此如果我們把CPU的速度比作戰斗機的話,那么I/O操作的速度就是肯德雞。

也就是說當程序跑起來時(CPU執行機器指令),其速度是要遠遠快于I/O速度。那么接下來的問題就是二者速度相差這么大,該如何設計、更加合理的高效利用系統資源呢?

既然有速度差異,進程在執行完I/O操作前不能繼續向前推進,那就只有等待(wait)。

三、執行I/O時底層都發生了什么

在支持線程的操作系統中,實際上被調度的是線程而不是進程,為了更加清晰的理解I/O過程,暫時假設操作系統只有進程這樣的概念,先不去考慮線程。

如下圖所示,現在內存中有兩個進程,進程A和進程B,當前進程A正在運行。如下圖所示:

pYYBAGPAt4KAYt25AABEuvp-fNA932.png

進程A中有一段讀取文件的代碼,不管在什么語言中通常定義一個用來裝數據的buff,然后調用read之類的函數。

注意:與CPU執行指令的速度相比,I/O操作操作是非常慢的,因此操作系統是不可能把寶貴的CPU計算資源浪費在無謂的等待上的。由于外部設備執行I/O操作是相當慢的,所以在I/O操作完成之前進程是無法繼續向前推進的,這就是所謂的阻塞,即block。

只需記錄下當前進程的運行狀態并把CPU的PC寄存器指向其它進程的指令就

操作系統檢測到進程向I/O設備發起請求后就暫停進程的運行

。進程有暫停就會有繼續執行,因此操作系統必須保存被暫停的進程以備后續繼續執行,顯然我們可以用隊列來保存被暫停執行的進程。

poYBAGPAt4KAIICBAABb1qWMfiM798.png

如上圖所示,操作系統已經向磁盤發送I/O請求,因此磁盤driver開始將磁盤中的數據copy到進程A的buff中。雖然這時進程A已經被暫停執行了,但這并不妨礙磁盤向內存中copy數據。過程如下圖所示:

poYBAGPAt4KAVHH6AABf6xWYqj8841.png

操作系統中除了有阻塞隊列之外也有就緒隊列,所謂就緒隊列是指隊列里的進程準備就緒可以被CPU執行了。在即使只有1個核的機器上也可以創建出成千上萬個進程,CPU不可能同時執行這么多的進程,因此必然存在這樣的進程,即使其一切準備就緒也不能被分配到計算資源,這樣的進程就被放到了就緒隊列。

pYYBAGPAt4KAPJ92AABy_O2xHNc532.png

由于就緒隊列中還有嗷嗷待哺的進程B,所以當進程A被暫停執行后CPU是不可以閑下來的。這時操作系統開始在就緒隊列中找下一個可以執行的進程,也就是這里的進程B。此時操作系統將進程B從就緒隊列中取出,找出進程B被暫停時執行到的機器指令的位置,然后將CPU的PC寄存器指向該位置,這樣進程B就開始運行啦。

pYYBAGPAt4KAYiFhAABuXjxJy8I019.png

如上圖所示,進程B在被CPU執行,磁盤在向進程A的內存空間中copy數據,數據copy和指令執行在同時進行,在操作系統的調度下,CPU、磁盤都得到了充分的利用。此后磁盤將全部數據都copy到了進程A的內存中,操作系統接收到磁盤中斷后發現數據copy完畢,進程A重新獲得繼續運行的資格,操作系統把進程A從阻塞隊列放到了就緒隊列當中。

pYYBAGPAt4KAAO1iAABmWzVw4No756.png

此后進程B繼續執行,進程A繼續等待,進程B執行了一會兒后操作系統認為進程B執行的時間夠長了,因此把進程B放到就緒隊列,把進程A取出并繼續執行。操作系統把進程B放到的是就緒隊列,因此進程B被暫停運行僅僅是因為時間片到了而不是因為發起I/O請求被阻塞。

pYYBAGPAt4KAQ_sTAABphwHVJHM245.png

四、零拷貝(Zero-copy)

值得注意的一點是:上面的講解中直接把磁盤數據copy到了進程空間中,但實際上一般情況下I/O數據是要首先copy到操作系統內部,然后操作系統再copy到進程空間中。性能要求很高的場景其實也是可以繞過操作系統直接進行數據copy,這種繞過操作系統直接進行數據copy的技術被稱為零拷貝(Zero-copy)。

I/O多路復用

本文我們詳細講解什么是I/O多路復用以及使用方法,這其中以epoll為代表的I/O多路復用(基于事件驅動)技術使用非常廣泛,實際上你會發現但凡涉及到高并發、高性能的場景基本上都能見到事件驅動的編程方法。

一、什么是文件?

Linux世界中文件是一個很簡單的概念,只需要將其理解為一個N byte的序列就可以了:

b1, b2, b3, b4, ....... bN

實際上所有的I/O設備都被抽象了,一切皆文件(Everything is File),磁盤、網絡數據、終端,甚至進程間通信工具管道pipe等都被當做文件對待。

pYYBAGPAt4GANIgNAABtxe1O5TM682.png

常用的I/O操作接口一般有以下幾類:

1、打開文件,open;

2、改變讀寫位置,seek;

3、文件讀寫,read、write;

4、關閉文件,close。

二、什么是文件描述符?

在上文中我們講到:要想進行I/O讀操作,像磁盤數據,需要指定一個buff用來裝入數據。在Linux世界要想使用文件,需要借助一個號碼,根據“弄不懂原則”,這個號碼就被稱為了文件描述符(file descriptors),在Linux世界中鼎鼎大名,其道理和上面那個排隊號碼一樣。文件描述僅僅就是一個數字而已,但是通過這個數字我們可以操作一個打開的文件。

poYBAGPAt4KACJ46AABeOs3qbNo223.png

有了文件描述符,進程可以對文件一無所知,比如文件在磁盤的什么位置、加載到內存中又是怎樣管理的等等,這些信息統統交由操作系統打理,進程無需關心,操作系統只需要給進程一個文件描述符就足夠了。

三、文件描述符太多了怎么辦?

從上文中我們知道,所有I/O操作都可以通過文件樣的概念來進行,這當然包括網絡通信

如果你有一個IM服務器,當三次握手建議長連接成功以后,我們會調用accept來獲取一個鏈接,調用該函數我們同樣會得到一個文件描述符,通過這個文件描述符就可以處理客戶端發送的聊天消息并且把消息轉發給接收者。

也就是說,通過這個描述符就可以和客戶端進行通信了:

// 通過accept獲取客戶端的文件描述符
int conn_fd = accept(...);

Server端的處理邏輯通常是接收客戶端消息數據,然后執行轉發(給接收者)邏輯:

if(read(conn_fd, msg_buff) > 0) {
do_transfer(msg_buff);
}

既然主題是高并發,那么Server端就不可能只和一個客戶端通信,而是可能會同時和成千上萬個客戶端進行通信。這時需要處理不再是一個描述符這么簡單,而是有可能要處理成千上萬個描述符。為了不讓問題一上來就過于復雜先簡單化,假設只同時處理兩個客戶端的請求。

有的同學可能會說,這還不簡單,這樣寫不就行了:

if(read(socket_fd1, buff) > 0) { // 處理第一個
do_transfer();
}
if(read(socket_fd2, buff) > 0) { // 處理第二個
do_transfer();

如果此時沒有數據可讀那么進程會被阻塞而暫停運行。這時我們就無法處理第二個請求了,即使第二個請求的數據已經就位,這也就意味著處理某一個客戶端時由于進程被阻塞導致剩下的所有其它客戶端必須等待,在同時處理幾萬客戶端的server上。這顯然是不能容忍的。

聰明的你一定會想到使用多線程:為每個客戶端請求開啟一個線程,這樣一個客戶端被阻塞就不會影響到處理其它客戶端的線程了。注意:既然是高并發,那么我們要為成千上萬個請求開啟成千上萬個線程嗎,大量創建銷毀線程會嚴重影響系統性能。

那么這個問題該怎么解決呢?

這里的關鍵點在于:我們事先并不知道一個文件描述對應的I/O設備是否是可讀的、是否是可寫的,在外設的不可讀或不可寫的狀態下進行I/O只會導致進程阻塞被暫停運行。

三、I/O多路復用(I/O multiplexing)

multiplexing一詞多用于通信領域,為了充分利用通信線路,希望在一個信道中傳輸多路信號,要想在一個信道中傳輸多路信號就需要把這多路信號結合為一路,將多路信號組合成一個信號的設備被稱為Multiplexer(多路復用器),顯然接收方接收到這一路組合后的信號后要恢復原先的多路信號,這個設備被稱為Demultiplexer(多路分用器)。

如下圖所示:

pYYBAGPAt4KAbEryAAAoSHU-V9M362.png

所謂I/O多路復用指的是這樣一個過程:

1、拿到一堆文件描述符(不管是網絡相關的、還是磁盤文件相關等等,任何文件描述符都可以);

2、通過調用某個函數告訴內核:“這個函數你先不要返回,你替我監視著這些描述符,當這堆文件描述符中有可以進行I/O讀寫操作的時候你再返回”;

3、當調用的這個函數返回后就能知道哪些文件描述符可以進行I/O操作了。
**
三、I/O多路復用三劍客**

由于調用這些I/O多路復用函數時如果任何一個需要監視的文件描述符都不可讀或者可寫那么進程會被阻塞暫停執行,直到有文件描述符可讀或者可寫才繼續運行。所以Linux上的select、poll、epoll都是阻塞式I/O,也就是同步I/O。

1、select:初出茅廬

在select I/O多路復用機制下,需要把想監控的文件描述集合通過函數參數的形式告訴select,然后select將這些文件描述符集合拷貝到內核中。為了減少這種數據拷貝帶來的性能損耗,Linux內核對集合的大小做了限制,并規定用戶監控的文件描述集合不能超過1024個,同時當select返回后,僅僅能知道有些文件描述符可以讀寫了。

select的特點

1、能照看的文件描述符數量有限,不能超過1024個;

2、用戶給文件描述符需要拷貝的內核中;

3、只能告訴有文件描述符滿足要求但不知道是哪個。

2、poll:小有所成

poll和select是非常相似,相對于select的優化僅僅在于解決文件描述符不能超過1024個的限制,select和poll都會隨著監控的文件描述數量增加而性能下降,因此不適合高并發場景。

3、epoll:獨步天下

在select面臨的三個問題中,文件描述數量限制已經在poll中解決了,剩下的兩個問題呢?

針對拷貝問題

epoll使用的策略是各個擊破與共享內存。文件描述符集合的變化頻率比較低,select和poll頻繁的拷貝整個集合,epoll通過引入epoll_ctl很體貼的做到了只操作那些有變化的文件描述符。同時epoll和內核還成為了好朋友,共享了同一塊內存,這塊內存中保存的就是那些已經可讀或者可寫的的文件描述符集合,這樣就減少了內核和程序的拷貝開銷。

針對需要遍歷文件描述符才能知道哪個可讀可寫的問題,epoll使用的策略是在select和poll機制下:進程要親自下場去各個文件描述符上等待,任何一個文件描述可讀或者可寫就喚醒進程,但是進程被喚醒后也是一臉懵逼并不知道到底是哪個文件描述符可讀或可寫,還要再從頭到尾檢查一遍。在epoll機制下進程不需要親自下場了,進程只要等待在epoll上,epoll代替進程去各個文件描述符上等待,當哪個文件描述符可讀或者可寫的時候就告訴epoll,由epoll記錄。

在epoll這種機制下,實際上利用的就是“不要打電話給我,有需要我會打給你”這種策略,進程不需要一遍一遍麻煩的問各個文件描述符,而是翻身做主人了——“你們這些文件描述符有哪個可讀或者可寫了主動報上來”。

同步與異步
**
一、同步與異步場景:打電話與發郵件**

1、同步

通常打電話時都是一個人在說另一個人聽,一個人在說的時候另一個人等待,等另一個人說完后再接著說,因此在這個場景中你可以看到,“依賴”、“關聯”、“等待”這些關鍵詞出現了,因此打電話這種溝通方式就是所謂的同步。

pYYBAGPAt4KATn2GAABHxp60zds376.png

2、異步

另一種常用的溝通方式是郵件,因為沒有人傻等著你寫郵件什么都不做,因此你可以慢慢悠悠的寫,當你在寫郵件時收件人可以去做一些像摸摸魚啊、上個廁所、和同時抱怨一下為什么十一假期不放兩周之類有意義的事情。同時當你寫完郵件發出去后也不需要干巴巴的等著對方回復什么都不做,你也可以做一些像摸魚之類這樣有意義的事情。

poYBAGPAt4KAXNgbAABbe_LqRH4967.png

在這里,你寫郵件別人摸魚,這兩件事又在同時進行,收件人和發件人都不需要相互等待,發件人寫完郵件的時候簡單的點個發送就可以了,收件人收到后就可以閱讀啦,收件人和發件人不需要相互依賴、不需要相互等待。因此郵件這種溝通方式就是異步的。

二、編程中的同步調用

一般的函數調用都是同步的,就像這樣:

funcA調用funcB,那么在funcB執行完前,funcA中的后續代碼都不會被執行,也就是說funcA必須等待funcB執行完成,如下圖所示。

poYBAGPAt4KADAZPAAA18Gc7uis380.png

從上圖中可以看出,在funcB運行期間funcA什么都做不了,這就是典型的同步。一般來說,像這種同步調用,funcA和funcB是運行在同一個線程中的,但值得注意的是即使運行在兩個不能線程中的函數也可以進行同步調用,像我們進行IO操作時實際上底層是通過系統調用的方式向操作系統發出請求。

pYYBAGPAt4KAWG1wAAA4AORS9xM330.png

如上圖所示,只有當read函數返回后程序才可以被繼續執行。和上面的同步調用不同的是,函數和被調函數運行在不同的線程中。由此我們可以得出結論,同步調用和函數與被調函數是否運行在同一個線程是沒有關系的。在這里需要再次強調同步方式下函數和被調函數無法同時進行。

三、編程中的異步調用

有同步調用就有異步調用。一般來說異步調用總是和I/O操作等耗時較高的任務如影隨形,像磁盤文件讀寫、網絡數據的收發、數據庫操作等。

在這里以磁盤文件讀取為例,在read函數的同步調用方式下,文件讀取完之前調用方是無法繼續向前推進的,但如果read函數可以異步調用情況就不一樣了。假如read函數可以異步調用的話,即使文件還沒有讀取完成,read函數也可以立即返回。

pYYBAGPAt4KAPYFVAAA2sskmMYs650.png

如上圖所示,在異步調用方式下,調用方不會被阻塞,函數調用完成后可以立即執行接下來的程序。這時異步的重點在于調用方接下來的程序執行可以和文件讀取同時進行。值得注意的是異步調用對于程序員來說在理解上是一種負擔,代碼編寫上更是一種負擔,總的來說,上帝在為你打開一扇門的時候會適當的關上一扇窗戶。

有的同學可能會問,在同步調用下,調用方不再繼續執行而是暫停等待,被調函數執行完后很自然的就是調用方繼續執行,那么異步調用下調用方怎知道被調函數是否執行完成呢?這就分為調用方根本就不關心執行結果和調用方需要知道執行結果兩種情況。

第一種情況比較簡單,無需討論。

第二種情況下就比較有趣了,通常有兩種實現方式:

1、通知機制

當任務執行完成后發送信號來通知調用方任務完成(這里的信號有很多實現方式:Linux中的signal,或使用信號量等機制都可實現);

2、回調機制:
也就是常說的callback。

四、具體的編程例子中理解同步和異步

以常見Web服務為例來說明這個問題。一般來說Web Server接收到用戶請求后會有一些典型的處理邏輯,最常見的就是數據庫查詢(當然,你也可以把這里的數據庫查詢換成其它I/O操作,比如磁盤讀取、網絡通信等),在這里假定處理一次用戶請求需要經過步驟A、B、C,然后讀取數據庫,數據庫讀取完成后需要經過步驟D、E、F。

其中步驟A、B、C和D、E、F不需要任何I/O,也就是說這六個步驟不需要讀取文件、網絡通信等,涉及到I/O操作的只有數據庫查詢這一步。一般來說Web Server有主線程和數據庫處理線程兩個典型的線程。

首先我們來看下最簡單的實現方式,也就是同步。

這種方式最為自然也最為容易理解:

主線程在發出數據庫查詢請求后就會被阻塞而暫停運行,直到數據庫查詢完畢后面的D、E、F才可以繼續運行,這就是最為典型的同步方法。

poYBAGPAt4KAVIn6AABIlNCcQO8682.png

如上圖所示,主線程中會有“空隙”,這個空隙就是主線程的“休閑時光”,主線程在這段休閑時光中需要等待數據庫查詢完成才能繼續后續處理流程。在這里主線程就好比監工的老板,數據庫線程就好比苦逼搬磚的程序員,在搬完磚前老板什么都不做只是緊緊的盯著你,等你搬完磚后才去忙其它事情。

1、異步情況:主線程不關心數據庫操作結果

如下圖所示,主線程根本就不關心數據庫是否查詢完畢,數據庫查詢完畢后自行處理接下來的D、E、F三個步驟。

pYYBAGPAt4KAZ_59AABSS61GMSE289.png

一個請求通常需要經過七個步驟,其中前三個是在主線程中完成的,后四個是在數據庫線程中完成的,數據庫線程通過回調函數查完數據庫后處理D、E、F幾個步驟。

偽碼如下:

主線程處理請求和數據庫處理查詢請求可以同時進行,從系統性能上看能更加充分的利用系統資源,更加快速的處理請求;從用戶的角度看,系統的響應也會更加迅速。這就是異步的高效之處。但可以看出,異步編程并不如同步來的容易理解,系統可維護性上也不如同步模式。

2、異步情況:主線程關心數據庫操作結果

如下圖所示,數據庫線程需要將查詢結果利用通知機制發送給主線程,主線程在接收到消息后繼續處理上一個請求的后半部分。

pYYBAGPAt4KAe_GoAABLC9RwSqk895.png

由此我們可以看到:ABCDEF幾個步驟全部在主線中處理,同時主線程同樣也沒有了“休閑時光”,只不過在這種情況下數據庫線程是比較清閑的,從這里并沒有上一種方法高效,但是依然要比同步模式下要高效。但是要注意的是并不是所有的情況下異步都一定比同步高效,還需要結合具體業務以及IO的復雜度具體情況具體分析。

高并發中的協程

協程是高性能高并發編程中不可或缺的技術,包括即時通訊(IM系統)在內的互聯網產品應用產品中應用廣泛,比如號稱支撐微信海量用戶的后臺框架就是基于協程打造的。而且越來越多的現代編程語言都將協程視為最重要的語言技術特征,已知的包括:Go、Python、Kotlin等。

一、從普通函數到協程

普通函數下,只有當執行完print("c")這句話后函數才會返回,但是在協程下當執行完print("a")后func就會因“暫停并返回”這段代碼返回到調用函數。

我寫一個return也能返回,就像這樣:

直接寫一個return語句確實也能返回,但這樣寫的話return后面的代碼都不會被執行到了。

協程之所以神奇就神奇在當我們從協程返回后還能繼續調用該協程,并且是從該協程的上一個返回點后繼續執行。

就好比孫悟空說一聲“定”,函數就被暫停了:

這時我們就可以返回到調用函數,當調用函數什么時候想起該協程后可以再次調用該協程,該協程會從上一個返回點繼續執行。值得注意的是當普通函數返回后,進程的地址空間中不會再保存該函數運行時的任何信息,而協程返回后,函數的運行時信息是需要保存下來的。

二、“Talk is cheap,show me the code”

在python語言中,這個“定”字同樣使用關鍵詞yield。

這樣我們的func函數就變成了:

這時我們的func就不再是簡簡單單的函數了,而是升級成為了協程,那么我們該怎么使用呢?

很簡單:

雖然func函數沒有return語句,也就是說雖然沒有返回任何值,但是我們依然可以寫co = func()這樣的代碼,意思是說co就是拿到的協程了。

接下來調用該協程,使用next(co),運行函數A看看執行到第3行的結果是什么:

顯然,和預期一樣協程func在print("a")后因執行yield而暫停并返回函數A。

接下來是第4行,這個毫無疑問,A函數在做一些自己的事情,因此會打印:

接下來是重點的一行,當執行第5行再次調用協程時該打印什么呢?

如果func是普通函數,那么會執行func的第一行代碼,也就是打印a。

但func不是普通函數,而是協程,我們之前說過,協程會在上一個返回點繼續運行,因此這里應該執行的是func函數第一個yield之后的代碼,也就是 print("b")。

三、圖形化解釋

為了更加徹底的理解協程,我們使用圖形化的方式再看一遍。

首先是普通的函數調用:

poYBAGPAt4GAHcnSAAA5V9L5aqU699.png

在該圖中方框內表示該函數的指令序列,如果該函數不調用任何其它函數,那么應該從上到下依次執行,但函數中可以調用其它函數,因此其執行并不是簡單的從上到下,箭頭線表示執行流的方向。

從上圖中可以看到:首先來到funcA函數,執行一段時間后發現調用了另一個函數funcB,這時控制轉移到該函數,執行完成后回到main函數的調用點繼續執行。這是普通的函數調用。

接下來是協程:

poYBAGPAt4KAVN7eAAA7ORDX0a4669.png

在這里依然首先在funcA函數中執行,運行一段時間后調用協程,協程開始執行,直到第一個掛起點,此后就像普通函數一樣返回funcA函數,funcA函數執行一些代碼后再次調用該協程。

三、函數只是協程的一種特例

和普通函數不同的是,協程能知道自己上一次執行到了哪里。協程會在函數被暫停運行時保存函數的運行狀態,并可以從保存的狀態中恢復并繼續運行。

四、協程的歷史

協程這種概念早在1958年就已經提出來了,要知道這時線程的概念都還沒有提出來。到了1972年,終于有編程語言實現了這個概念,這兩門編程語言就是Simula 67 以及Scheme。但協程這個概念始終沒有流行起來,甚至在1993年還有人考古一樣專門寫論文挖出協程這種古老的技術。

因為這一時期還沒有線程,如果你想在操作系統寫出并發程序那么你將不得不使用類似協程這樣的技術,后來線程開始出現,操作系統終于開始原生支持程序的并發執行,就這樣,協程逐漸淡出了程序員的視線。 直到近些年,隨著互聯網的發展,尤其是移動互聯網時代的到來,服務端對高并發的要求越來越高,協程再一次重回技術主流,各大編程語言都已經支持或計劃開始支持協程。

五、協程到底如何實現?

讓我們從問題的本質出發來思考這個問題協程的本質是什么呢? 協程之所以可以被暫停也可以繼續,那么一定要記錄下被暫停時的狀態,也就是上下文,當繼續運行的時候要恢復其上下文(狀態)函數運行時所有的狀態信息都位于函數運行時棧中。 如下圖所示,函數運行時棧就是需要保存的狀態,也就是所謂的上下文。

poYBAGPAt4KAFG-pAABYgbaix_Y673.png

從上圖中可以看出,該進程中只有一個線程,棧區中有四個棧幀,main函數調用A函數,A函數調用B函數,B函數調用C函數,當C函數在運行時整個進程的狀態就如圖所示。

再仔細想一想,為什么我們要這么麻煩的來回copy數據呢? 我們需要做的是直接把協程的運行需要的棧幀空間直接開辟在堆區中,這樣都不用來回copy數據了,如下圖所示。

poYBAGPAt4KARW4ZAABtjwx0dhQ451.png

從上圖中可以看到該程序中開啟了兩個協程,這兩個協程的棧區都是在堆上分配的,這樣我們就可以隨時中斷或者恢復協程的執行了。 進程地址空間最上層的棧區現在的作用是用來保存函數棧幀的,只不過這些函數并不是運行在協程而是普通線程中的。

在上圖中實際上共有一個普通線程和兩個協程3個執行流。 雖然有3個執行流但我們創建了幾個線程呢? 答案是:一個線程。

使用協程理論上我們可以開啟無數并發執行流,只要堆區空間足夠,同時還沒有創建線程的開銷,所有協程的調度、切換都發生在用戶態,這就是為什么協程也被稱作用戶態線程的原因所在。 所以即使創建了N多協程,但在操作系統看來依然只有一個線程,也就是說協程對操作系統來說是不可見的。

這也許是為什么協程這個概念比線程提出的要早的原因,可能是寫普通應用的程序員比寫操作系統的程序員最先遇到需要多個并行流的需求,那時可能都還沒有操作系統的概念,或者操作系統沒有并行這種需求,所以非操作系統程序員只能自己動手實現執行流,也就是協程。

六、協程技術概念小結

1、協程是比線程更小的執行單元

協程是比線程更小的一種執行單元可以認為是輕量級的線程。 之所以說輕的其中一方面的原因是協程所持有的棧比線程要小很多,java當中會為每個線程分配1M左右的棧空間,而協程可能只有幾十或者幾百K,棧主要用來保存函數參數、局部變量和返回地址等信息。

我們知道而線程的調度是在操作系統中進行的,而協程調度則是在用戶空間進行的,是開發人員通過調用系統底層的執行上下文相關api來完成的。 有些語言,比如nodejs、go在語言層面支持了協程,而有些語言,比如C,需要使用第三方庫才可以擁有協程的能力。

由于線程是操作系統的最小執行單元,因此也可以得出,協程是基于線程實現的,協程的創建、切換、銷毀都是在某個線程中來進行的。 使用協程是因為線程的切換成本比較高,而協程在這方面很有優勢。

2、協程的切換到底為什么很廉價?

關于這個問題,回顧一下線程切換的過程:

1)線程在進行切換的時候,需要將CPU中的寄存器的信息存儲起來,然后讀入另外一個線程的數據,這個會花費一些時間;

2)CPU的高速緩存中的數據,也可能失效,需要重新加載;

3)線程的切換會涉及到用戶模式到內核模式的切換,據說每次模式切換都需要執行上千條指令,很耗時。

實際上協程的切換之所以快的原因主要是:

1)在切換的時候,寄存器需要保存和加載的數據量比較小;

2)高速緩存可以有效利用;

3)沒有用戶模式到內核模式的切換操作;

4)更有效率的調度,因為協程是非搶占式的,前一個協程執行完畢或者堵塞,才會讓出CPU,而線程則一般使用了時間片的算法,會進行很多沒有必要的切換。

高性能服務器到底是如何實現的?

當你在閱讀文章的時候,有沒有想過,服務器是怎么把這篇文章發送給你的呢? 說起來很簡單不就是一個用戶請求嗎? 服務器根據請求從數據庫中撈出這篇文章,然后通過網絡發回去嗎。 其實有點復雜服務器端到底是如何并行處理成千上萬個用戶請求的呢? 這里面又涉及到哪些技術呢?

一、多進程

歷史上最早出現也是最簡單的一種并行處理多個請求的方法就是利用多進程。 比如在Linux世界中,可以使用fork、exec等系統調用創建多個進程,可以在父進程中接收用戶的連接請求,然后創建子進程去處理用戶請求。

poYBAGPAt4KAZBrqAABhEzsCA2E404.png

1、多進程并行處理的優點

1)編程簡單,非常容易理解;

2)由于各個進程的地址空間是相互隔離的,因此一個進程崩潰后并不會影響其它進程;

3)充分利用多核資源。

2、多進程并行處理的缺點

1)各個進程地址空間相互隔離,這一優點也會變成缺點,那就是進程間要想通信就會變得比較困難,你需要借助進程間通信機制,想一想你現在知道哪些進程間通信機制,然后讓你用代碼實現呢? 顯然,進程間通信編程相對復雜,而且性能也是一大問題;

2)創建進程開銷是比線程要大的,頻繁的創建銷毀進程無疑會加重系統負擔。

二、多線程

由于線程共享進程地址空間,因此線程間通信天然不需要借助任何通信機制,直接讀取內存就好了。 線程創建銷毀的開銷也變小了,要知道線程就像寄居蟹一樣,房子(地址空間)都是進程的,自己只是一個租客,因此非常的輕量級,創建銷毀的開銷也非常小。

我們可以為每個請求創建一個線程,即使一個線程因執行I/O操作——比如讀取數據庫等——被阻塞暫停運行也不會影響到其它線程。

pYYBAGPAt4KACXKKAABV6BFiBUo257.png

由于線程共享進程地址空間,這在為線程間通信帶來便利的同時也帶來了無盡的麻煩。 正是由于線程間共享地址空間,因此一個線程崩潰會導致整個進程崩潰退出,同時線程間通信簡直太簡單了,簡單到線程間通信只需要直接讀取內存就可以了,也簡單到出現問題也極其容易,死鎖、線程間的同步互斥、等等,這些極容易產生bug,無數程序員寶貴的時間就有相當一部分用來解決多線程帶來的無盡問題。

雖然線程也有缺點,但是相比多進程來說,線程更有優勢,但想單純的利用多線程就能解決高并發問題也是不切實際的。因為雖然線程創建開銷相比進程小,但依然也是有開銷的,對于動輒數萬數十萬的鏈接的高并發服務器來說,創建數萬個線程會有性能問題,這包括內存占用、線程間切換,也就是調度的開銷。

三、事件驅動:Event Loop

到目前為止,提到“并行”二字就會想到進程、線程。但是并行編程只能依賴這兩項技術嗎?并不是這樣的!還有另一項并行技術廣泛應用在GUI編程以及服務器編程中,這就是近幾年非常流行的事件驅動編程:event-based concurrency。

大家不要覺得這是一項很難懂的技術,實際上事件驅動編程原理上非常簡單。

這一技術需要兩種原料:

1)event;

2)處理event的函數,這一函數通常被稱為event handler;

pYYBAGPAt4KAF4TgAABIiAGLZ6o299.png

由于對于網絡通信服務器來說,處理一個用戶請求時大部分時間其實都用在了I/O操作上,像數據庫讀寫、文件讀寫、網絡讀寫等。當一個請求到來,簡單處理之后可能就需要查詢數據庫等I/O操作,我們知道I/O是非常慢的,當發起I/O后我們大可以不用等待該I/O操作完成就可以繼續處理接下來的用戶請求。所以一個event loop可以同時處理多個請求。

pYYBAGPAt4KACuY4AABBxDUB2mo119.png

四、事件來源:IO多路復用

IO多路復用技術通過一次監控多個文件描述,當某個“文件”(實際可能是im網絡通信中socket)可讀或者可寫的時候我們就能同時處理多個文件描述符啦。

這樣IO多路復用技術就成了event loop的原材料供應商,源源不斷的給我們提供各種event,這樣關于event來源的問題就解決了。

pYYBAGPAt4KACZQ1AABT4u4xJFw848.png

五、問題:阻塞式IO

當我們進行IO操作,比如讀取文件時,如果文件沒有讀取完成,那么我們的程序(線程)會被阻塞而暫停執行,這在多線程中不是問題,因為操作系統還可以調度其它線程。 但是在單線程的event loop中是有問題的,原因就在于當我們在event loop中執行阻塞式IO操作時整個線程(event loop)會被暫停運行,這時操作系統將沒有其它線程可以調度,因為系統中只有一個event loop在處理用戶請求,這樣當event loop線程被阻塞暫停運行時所有用戶請求都沒有辦法被處理。 你能想象當服務器在處理其它用戶請求讀取數據庫導致你的請求被暫停嗎?

pYYBAGPAt4KAXHabAABMB0i4FqQ484.png

因此:在基于事件驅動編程時有一條注意事項,那就是不允許發起阻塞式IO。 有的同學可能會問,如果不能發起阻塞式IO的話,那么該怎樣進行IO操作呢?

六、解決方法:非阻塞式IO

為克服阻塞式IO所帶來的問題,現代操作系統開始提供一種新的發起IO請求的方法,這種方法就是異步IO。 對應的,阻塞式IO就是同步IO,關于同步和異步詳見上文。

異步IO時,假設調用aio_read函數(具體的異步IO API請參考具體的操作系統平臺),也就是異步讀取,當我們調用該函數后可以立即返回,并繼續其它事情,雖然此時該文件可能還沒有被讀取,這樣就不會阻塞調用線程了。 此外,操作系統還會提供其它方法供調用線程來檢測IO操作是否完成。

七、基于事件驅動并行編程的難點

雖然有異步IO來解決event loop可能被阻塞的問題,但是基于事件編程依然是困難的。

首先event loop是運行在一個線程中的,顯然一個線程是沒有辦法充分利用多核資源的,有的同學可能會說那就創建多個event loop實例不就可以了,這樣就有多個event loop線程了,但是這樣一來多線程問題又會出現。

其次在于編程方面,異步編程需要結合回調函數(這種編程方式需要把處理邏輯分為兩部分:一部分調用方自己處理,另一部分在回調函數中處理),這一編程方式的改變加重了程序員在理解上的負擔,基于事件編程的項目后期會很難擴展以及維護。

八、更好的方法

有沒有一種方法既能結合同步IO的簡單理解又不會因同步調用導致線程被阻塞呢? 答案是肯定的,這就是用戶態線程(user level thread),也就是大名鼎鼎的協程。

雖然基于事件編程有這樣那樣的缺點,但是在當今的高性能高并發服務器上基于事件編程方式依然非常流行,但已經不是純粹的基于單一線程的事件驅動了,而是 event loop + multi thread + user level thread。

進程、線程、協程

一、什么是進程?

1、基本常識

計算機的核心是CPU,它承擔了所有的計算任務; 操作系統是計算機的管理者,它負責任務的調度、資源的分配和管理,統領整個計算機硬件; 應用程序則是具有某種功能的程序,程序是運行于操作系統之上的。

進程是一個具有一定獨立功能的程序在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。 進程是一種抽象的概念,從來沒有統一的標準定義。

進程一般由程序、數據集合和進程控制塊三部分組成:

程序用于描述進程要完成的功能,是控制進程執行的指令集;

數據集合是程序在執行時所需要的數據和工作區;

程序控制塊(Program Control Block,簡稱PCB),包含進程的描述信息和控制信息,是進程存在的唯一標志。

進程的特點:

動態性:進程是程序的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的;

并發性:任何進程都可以同其他進程一起并發執行;

獨立性:進程是系統進行資源分配和調度的一個獨立單位;

結構性:進程由程序、數據和進程控制塊三部分組成。

2、為什么要有多進程?

多進程目的是提高cpu的使用率。 假設只有一個進程(先不談多線程),從操作系統的層面看,我們使用打印機的步驟有如下:

1)使用CPU執行程序,去硬盤讀取需要打印的文件,然后CPU會長時間的等待,直到硬盤讀寫完成;

2)使用CPU執行程序,讓打印機打印這些內容,然后CPU會長時間的等待,等待打印結束。

在這樣的情況下:其實CPU的使用率其實非常的低。

打印一個文件從頭到尾需要的時間可能是1分鐘,而cpu使用的時間總和可能加起來只有幾秒鐘。 而后面如果單進程執行游戲的程序的時候,CPU也同樣會有大量的空閑時間。

使用多進程后:

當CPU在等待硬盤讀寫文件,或者在等待打印機打印的時候,CPU可以去執行游戲的程序,這樣CPU就能盡可能高的提高使用率。

再具體一點說,其實也提高了效率。 因為在等待打印機的時候,這時候顯卡也是閑置的,如果用多進程并行的話,游戲進程完全可以并行使用顯卡,并且與打印機之間也不會互相影響。

3、總結

進程直觀點說是保存在硬盤上的程序運行以后,會在內存空間里形成一個獨立的內存體,這個內存體有自己獨立的地址空間,有自己的堆,上級掛靠單位是操作系統。 操作系統會進程為單位,分配系統資源(CPU時間片、內存等資源),進程是資源分配的最小單位。

二、什么是線程?

1、基本常識

早期操作系統中并沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。 任務調度采用的是時間片輪轉的搶占式調度方式,而進程是任務調度的最小單位,每個進程有各自獨立的一塊內存,使得各個進程之間內存地址相互隔離。 后來隨著計算機的發展,對CPU的要求越來越高,進程之間的切換開銷較大,已經無法滿足越來越復雜的程序的要求了。 于是就發明了線程。

線程是程序執行中一個單一的順序控制流程:

1)程序執行流的最小單元

2)處理器調度和分派的基本單位

一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間)。 一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成。 而進程由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。

poYBAGPAt4KAbQWmAAC2YGmaw5k758.png

如上圖所示,在任務管理器的進程一欄里,有道詞典和有道云筆記就是進程,而在進程下又有著多個執行不同任務的線程。

2、任務調度

線程是什么? 要理解這個概念,需要先了解一下操作系統的一些相關概念。 大部分操作系統(如Windows、Linux)的任務調度是采用時間片輪轉的搶占式調度方式。 在一個進程中:當一個線程任務執行幾毫秒后,會由操作系統的內核(負責管理各個任務)進行調度,通過硬件的計數器中斷處理器,讓該線程強制暫停并將該線程的寄存器放入內存中,通過查看線程列表決定接下來執行哪一個線程,并從內存中恢復該線程的寄存器,最后恢復該線程的執行,從而去執行下一個任務。

上述過程中任務執行的那一小段時間叫做時間片,任務正在執行時的狀態叫運行狀態,被暫停的線程任務狀態叫做就緒狀態,意為等待下一個屬于它的時間片的到來。

這種方式保證了每個線程輪流執行,由于CPU的執行效率非常高,時間片非常短,在各個任務之間快速地切換,給人的感覺就是多個任務在“同時進行”,這也就是我們所說的并發(別覺得并發有多高深,它的實現很復雜,但它的概念很簡單,就是一句話:多個任務同時執行)。

pYYBAGPAt4KAF1ItAAA5JMFTsa8168.png

3、進程與線程的區別

進程與線程的關系

1)線程是程序執行的最小單位,而進程是操作系統分配資源的最小單位;

2)一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線;

3)進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號),某進程內的線程在其它進程不可見;

4)線程上下文切換比進程上下文切換要快得多。

poYBAGPAt4KAFdIVAABW0Jc_sM8711.png

▲ 進程與線程的資源共享關系

pYYBAGPAt4KAA4pgAAA5SpLkwC8623.png

▲ 單線程與多線程的關系

總之線程和進程都是一種抽象的概念,線程是一種比進程更小的抽象,線程和進程都可用于實現并發。

在早期的操作系統中并沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。 它相當于一個進程里只有一個線程,進程本身就是線程。 所以線程有時被稱為輕量級進程。

后來隨著計算機的發展,對多個任務之間上下文切換的效率要求越來越高,就抽象出一個更小的概念——線程,一般一個進程會有多個(也可以是一個)線程。

pYYBAGPAt4KAAipnAAA-iS2yQbU128.png

4、多線程與多核

上面提到的時間片輪轉的調度方式說一個任務執行一小段時間后強制暫停去執行下一個任務,每個任務輪流執行。很多操作系統的書都說“同一時間點只有一個任務在執行”。其實“同一時間點只有一個任務在執行”這句話是不準確的,至少它是不全面的。那多核處理器的情況下,線程是怎樣執行呢?這就需要了解內核線程。

多核(心)處理器是指在一個處理器上集成多個運算核心從而提高計算能力,也就是有多個真正并行計算的處理核心,每一個處理核心對應一個內核線程。內核線程(Kernel Thread,KLT)就是直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操作調度器對線程進行調度,并負責將線程的任務映射到各個處理器上。

一般一個處理核心對應一個內核線程,比如單核處理器對應一個內核線程,雙核處理器對應兩個內核線程,四核處理器對應四個內核線程。

現在的電腦一般是雙核四線程、四核八線程,是采用超線程技術將一個物理處理核心模擬成兩個邏輯處理核心,對應兩個內核線程,所以在操作系統中看到的CPU數量是實際物理CPU數量的兩倍,如你的電腦是雙核四線程,打開“任務管理器 -> 性能”可以看到4個CPU的監視器,四核八線程可以看到8個CPU的監視器。

超線程技術就是利用特殊的硬件指令,把一個物理芯片模擬成兩個邏輯處理核心,讓單個處理器都能使用線程級并行計算,進而兼容多線程操作系統和軟件,減少了CPU的閑置時間,提高的CPU的運行效率。這種超線程技術(如雙核四線程)由處理器硬件的決定,同時也需要操作系統的支持才能在計算機中表現出來。

程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Lightweight Process,LWP),輕量級進程就是通常意義上所講的線程,也被叫做用戶線程。

由于每個輕量級進程都由一個內核線程支持,因此只有先支持內核線程,才能有輕量級進程。

用戶線程與內核線程的對應關系有三種模型:

1)一對一模型;

2)多對一模型;

3)多對多模型。

5、一對一模型

對于一對一模型來說:一個用戶線程就唯一地對應一個內核線程(反過來不一定成立,一個內核線程不一定有對應的用戶線程)。 這樣如果CPU沒有采用超線程技術(如四核四線程的計算機),一個用戶線程就唯一地映射到一個物理CPU的內核線程,線程之間的并發是真正的并發。

一對一模型優點

使用戶線程具有與內核線程一樣的優點一個線程因某種原因阻塞時其他線程的執行不受影響(此處,一對一模型也可以讓多線程程序在多處理器的系統上有更好的表現)。

一對一模型缺點

1)許多操作系統限制了內核線程的數量,因此一對一模型會使用戶線程的數量受到限制;

2)許多操作系統內核線程調度時,上下文切換的開銷較大,導致用戶線程的執行效率下降。

pYYBAGPAt4GANakzAAAu3ARYrWg246.png

▲ 一對一模型

6、多對一模型

多對一模型將多個用戶線程映射到一個內核線程上,線程之間的切換由用戶態的代碼來進行,系統內核感受不到線程的實現方式。 用戶線程的建立、同步、銷毀等都在用戶態中完成,不需要內核的介入。

多對一模型優點

1)多對一模型的線程上下文切換速度要快許多;

2)多對一模型對用戶線程的數量幾乎無限制。

多對一模型缺點

1)如果其中一個用戶線程阻塞,那么其它所有線程都將無法執行,因為此時內核線程也隨之阻塞了;

2)在多處理器系統上,處理器數量的增加對多對一模型的線程性能不會有明顯的增加,因為所有的用戶線程都映射到一個處理器上了。

poYBAGPAt4KAbJf3AAA0rnsT-Rk203.png

▲ 多對一模型

7、多對多模型

多對多模型結合了一對一模型和多對一模型的優點將多個用戶線程映射到多個內核線程上,由線程庫負責在可用的可調度實體上調度用戶線程。

這使得線程的上下文切換非常快,因為它避免了系統調用。 但是增加了復雜性和優先級倒置的可能性,以及在用戶態調度程序和內核調度程序之間沒有廣泛(且高昂)協調的次優調度。

多對多模型的優點

1)一個用戶線程的阻塞不會導致所有線程的阻塞,因為此時還有別的內核線程被調度來執行;

2)多對多模型對用戶線程的數量沒有限制;

3)在多處理器的操作系統中,多對多模型的線程也能得到一定的性能提升,但提升的幅度不如一對一模型的高。

poYBAGPAt4KAIE-hAAA9JueYCwg303.png

▲ 多對多模型

在現在流行的操作系統中,大都采用多對多的模型。

8、查看進程與線程

一個應用程序可能是多線程的,也可能是多進程的,如何查看呢?

在Windows下我們只須打開任務管理器就能查看一個應用程序的進程和線程數。 按“Ctrl+Alt+Del”或右鍵快捷工具欄打開任務管理器。

在“進程”選項卡下,我們可以看到一個應用程序包含的線程數。

如果一個應用程序有多個進程,我們能看到每一個進程,如在上圖中,Google的Chrome瀏覽器就有多個進程。

同時,如果打開了一個應用程序的多個實例也會有多個進程,如上圖中我打開了兩個cmd窗口,就有兩個cmd進程。 如果看不到線程數這一列,可以再點擊“查看 -> 選擇列”菜單,增加監聽的列。

查看CPU和內存的使用率:在性能選項卡中,我們可以查看CPU和內存的使用率,根據CPU使用記錄的監視器的個數還能看出邏輯處理核心的個數,如我的雙核四線程的計算機就有四個監視器。

pYYBAGPAt4KAX4O7AACNVgB6qWM391.png

▲ 查看CPU和內存的使用率

9、線程的生命周期

當線程的數量小于處理器的數量時,線程的并發是真正的并發,不同的線程運行在不同的處理器上。 但當線程的數量大于處理器的數量時,線程的并發會受到一些阻礙,此時并不是真正的并發,因為此時至少有一個處理器會運行多個線程。

在單個處理器運行多個線程時,并發是一種模擬出來的狀態。 操作系統采用時間片輪轉的方式輪流執行每一個線程。 現在,幾乎所有的現代操作系統采用的都是時間片輪轉的搶占式調度方式,如我們熟悉的Unix、Linux、Windows及macOS等流行的操作系統。

我們知道線程是程序執行的最小單位,也是任務執行的最小單位。 在早期只有進程的操作系統中,進程有五種狀態,創建、就緒、運行、阻塞(等待)、退出。 早期的進程相當于現在的只有單個線程的進程,那么現在的多線程也有五種狀態,現在的多線程的生命周期與早期進程的生命周期類似。

poYBAGPAt4KAYDdsAABMI2rJ8e4476.png

▲ 早期進程的生命周期

進程在運行過程有三種狀態:就緒、運行、阻塞,創建和退出狀態描述的是進程的創建過程和退出過程。

早期進程的生命周期:

創建:進程正在創建,還不能運行。操作系統在創建進程時要進行的工作包括分配和建立進程控制塊表項、建立資源表格并分配資源、加載程序并建立地址空間;

就緒:時間片已用完,此線程被強制暫停,等待下一個屬于它的時間片到來;

運行:此線程正在執行,正在占用時間片;

阻塞:也叫等待狀態,等待某一事件(如IO或另一個線程)執行完;

退出:進程已結束,所以也稱結束狀態,釋放操作系統分配的資源。

poYBAGPAt4KAGNuKAABEl4ojtsE578.png

▲ 線程的生命周期

線程的生命周期跟進程很類似:

創建:一個新的線程被創建,等待該線程被調用執行;

就緒:時間片已用完,此線程被強制暫停,等待下一個屬于它的時間片到來;

運行:此線程正在執行,正在占用時間片;

阻塞:也叫等待狀態,等待某一事件(如IO或另一個線程)執行完;

退出:一個線程完成任務或者其他終止條件發生,該線程終止進入退出狀態,退出狀態釋放該線程所分配的資源。

五、什么是協程?

1、基本常識

協程是一種基于線程之上,但又比線程更加輕量級的存在,這種由程序員自己寫程序來管理的輕量級線程叫做“用戶空間線程”,具有對內核來說不可見的特性。由于是自主開辟的異步任務,所以很多人也更喜歡叫它們纖程(Fiber),或者綠色線程(GreenThread)。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。

pYYBAGPAt4KAT0m0AADM7xHcYOU441.png

2、協程的目的

對于Java程序員來說,在傳統的J2EE系統中都是基于每個請求占用一個線程去完成完整的業務邏輯(包括事務)。所以系統的吞吐能力取決于每個線程的操作耗時。

如果遇到很耗時的I/O行為,則整個系統的吞吐立刻下降,因為這個時候線程一直處于阻塞狀態,如果線程很多的時候,會存在很多線程處于空閑狀態(等待該線程執行完才能執行),造成了資源應用不徹底。

最常見的例子就是JDBC(它是同步阻塞的),這也是為什么很多人都說數據庫是瓶頸的原因。這里的耗時其實是讓CPU一直在等待I/O返回,說白了線程根本沒有利用CPU去做運算,而是處于空轉狀態。而另外過多的線程,也會帶來更多的ContextSwitch開銷。

對于上述問題:現階段行業里的比較流行的解決方案之一就是單線程加上異步回調。其代表派是 node.js 以及 Java 里的新秀 Vert.x 。

而協程的目的就是當出現長時間的I/O操作時,通過讓出目前的協程調度,執行下一個任務的方式,來消除ContextSwitch上的開銷。

3、協程的特點

協程的特點總結一下就是:

1)線程的切換由操作系統負責調度,協程由用戶自己進行調度,因此減少了上下文切換,提高了效率;

2)線程的默認Stack大小是1M,而協程更輕量,接近1K。 因此可以在相同的內存中開啟更多的協程;

3)由于在同一個線程上,因此可以避免競爭關系而使用鎖;

4)適用于被阻塞的,且需要大量并發的場景。 但不適用于大量計算的多線程,遇到此種情況,更好實用線程去解決。

4、協程的原理

當出現IO阻塞的時候,由協程的調度器進行調度,通過將數據流立刻yield掉(主動讓出),并且記錄當前棧上的數據,阻塞完后立刻再通過線程恢復棧,并把阻塞的結果放到這個線程上去跑。

這樣看上去好像跟寫同步代碼沒有任何差別,這整個流程可以稱為coroutine,而跑在由coroutine負責調度的線程稱為Fiber。 比如Golang里的 go關鍵字其實就是負責開啟一個Fiber,讓func邏輯跑在上面。

由于協程的暫停完全由程序控制,發生在用戶態上; 而線程的阻塞狀態是由操作系統內核來進行切換,發生在內核態上。 因此協程的開銷遠遠小于線程的開銷,也就沒有了ContextSwitch上的開銷。

5、協程和線程的比較

pYYBAGPAt4KAC45EAACaYIy9M34037.png

六、總結

1、進程和線程的區別

1)調度:線程作為調度和分配的基本單位,進程作為擁有資源的基本單位;

2)并發性:不僅進程之間可以并發執行,同一個進程的多個線程之間也可并發執行;

3)擁有資源:進程是擁有資源的一個獨立單位,線程不擁有系統資源,但可以訪問隸屬于進程的資源;

4)系統開銷:在創建或撤消進程時,由于系統都要為之分配和回收資源,導致系統的開銷明顯大于創建或撤消線程時的開銷。

2、進程和線程的聯系

1)一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程;

2)資源分配給進程,同一進程的所有線程共享該進程的所有資源;

3)處理機分給線程,即真正在處理機上運行的是線程;

4)線程在執行過程中,需要協作同步。 不同進程的線程間要利用消息通信的辦法實現同步。

開發者在每個線程中只做非常輕量的操作,比如訪問一個極小的文件,下載一張極小的圖片,加載一段極小的文本等。 但是,這樣”輕量的操作“的量卻非常多。
在有大量這樣的輕量操作的場景下,即使可以通過使用線程池來避免創建與銷毀的開銷,但是線程切換的開銷也會非常大,甚至于接近操作本身的開銷。 對于這些場景,就非常需要一種可以減少這些開銷的方式。 于是,協程就應景而出,非常適合這樣的場景。

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • gpu
    gpu
    +關注

    關注

    28

    文章

    4904

    瀏覽量

    130591
  • 服務器
    +關注

    關注

    12

    文章

    9663

    瀏覽量

    87197
  • 線程
    +關注

    關注

    0

    文章

    507

    瀏覽量

    20067
  • 進程
    +關注

    關注

    0

    文章

    206

    瀏覽量

    14208
收藏 人收藏

    評論

    相關推薦
    熱點推薦

    解鎖高性能計算與區塊鏈應用,阿里云Kubernetes服務召喚神龍

    網絡功能,包括:EIP、SLB、防、安全組、HAVIP、NAT、用戶路由等眾多高級功能。容器服務計劃結合神龍(X-Dragon)彈性裸金屬服務器將來的多網卡支持,提供更原生的高性能
    發表于 06-13 15:52

    在DragonBoard 410c上實現并發處理TCP服務器

    服務,讓傳感和相關的控制設備接入,為此,本期blog將向大家介紹如何使用gevent高性能并發處理庫在draognbaord 410c上來實現一個
    發表于 09-25 15:53

    華為服務器為什么可以保持高性能和高可靠性

    通過第三方調研機構數據可以看出,華為服務器出貨量不斷攀升,得益于其持續通過高強度的研發投入和聚焦創新,從而為用戶提供可靠、高性能、簡單易用的計算平臺。
    發表于 08-02 07:19

    高性能服務器開發2018年的原創匯總

    高性能服務器開發 2018 年原創匯總
    發表于 06-10 12:33

    高性能并發服務器架構分享

    由于自己正在做一個高性能大用戶量的論壇程序,對高性能并發服務器架構比較感興趣,于是在網上收集了不少這方面的資料和大家分享。希望能和大家交流
    發表于 09-16 06:45

    如何在Dragonbaord 410c上實現高性能并發處理TCP服務器

    服務,讓傳感和相關的控制設備接入,為此,本期blog將向大家介紹如何使用gevent高性能并發處理庫在draognbaord 410c上來實現一個
    發表于 02-28 10:04 ?813次閱讀

    華為首款Arm架構服務器CPU鯤鵬920,業界最高性能Arm架構服務器CPU

    TaiShan系列服務器主要面向大數據、分布式存儲和ARM原生應用等場景,發揮ARM架構在多核、高能效等方面的優勢,為企業構建高性能、低功耗的新計算平臺;例如大數據場景,實現了多核并發
    的頭像 發表于 01-09 09:39 ?1.2w次閱讀

    如何理解服務器高性能

    :L1L2memorydiskinternet。 大家都知道IP是逐跳協議,也就是說我只能從一個路由,到下一個路由,再到下一個路由,如果你的電腦到服務器,中途要經過很多個路由
    的頭像 發表于 09-25 14:53 ?2100次閱讀

    詳解Nginx高性能的HTTP和反向代理服務器

    Nginx 是一個高性能的 HTTP 和反向代理服務器,特點是占用內存少,并發能力強,事實上 Nginx 的并發能力確實在同類型的網頁服務器
    的頭像 發表于 03-16 11:23 ?2671次閱讀

    高性能整流顯著提高服務器供電效率

    電子發燒友網站提供《高性能整流顯著提高服務器供電效率.pdf》資料免費下載
    發表于 07-26 09:46 ?0次下載
    <b class='flag-5'>高性能</b>整流<b class='flag-5'>器</b>顯著提高<b class='flag-5'>服務器</b>供電效率

    人工智能服務器高性能計算需求

    人工智能(AI)服務器是一種專門為了運行人工智能應用和提供大數據處理能力而設計的高性能計算機。它既可以支持本地應用程序和網頁,也可以為云和本地服務器提供復雜的AI模型和服務
    的頭像 發表于 12-08 09:44 ?707次閱讀

    國產高性能溫補晶振用于服務器光模塊,替換SiTime

    國產高性能溫補晶振用于服務器光模塊,替換SiTime
    的頭像 發表于 08-09 09:41 ?772次閱讀
    國產<b class='flag-5'>高性能</b>溫補晶振用于<b class='flag-5'>服務器</b>光模塊,替換SiTime

    GPU高性能服務器配置

    GPU高性能服務器作為提升計算速度和效率的關鍵設備,在各大應用場景中發揮著越來越重要的作用。在此,petacloud.ai小編為你介紹GPU高性能服務器的配置要點。
    的頭像 發表于 10-21 10:42 ?697次閱讀

    高性能服務器有什么用處?

    、游戲運行以及虛擬桌面部署等領域。通過動態擴展或縮減資源,高性能服務器能夠根據業務需求靈活調整計算能力,同時保證可用性和高效性能
    的頭像 發表于 11-04 10:22 ?473次閱讀

    Supermicro高性能服務器量產供貨,優化多重工作負載

    Supermicro, Inc.近日宣布其搭載Intel Xeon 6900系列性能核架構處理高性能服務器已開始量產供貨。 這一系列服務器
    的頭像 發表于 01-21 11:00 ?400次閱讀