1. 程式人生 > >30 張圖解 | 高頻面試知識點總結:面試官問我高併發服務模型哪家強?

30 張圖解 | 高頻面試知識點總結:面試官問我高併發服務模型哪家強?

文章每週持續更新,原創不易,「三連」讓更多人看到是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇)

面試中經常會被問到高效能服務模型選擇對比,以及如何提高服務效能和處理能力,這其中涉及作業系統軟體和計算機硬體知識,其實都是在考察面試者的基礎知識掌握程度,但如果沒準備的話容易一頭霧水,這次帶大家從頭到尾學習一遍,學完這一篇再也不怕面試官刨根問底了!

任務型別

談高併發服務模型選擇之前,我們先來看下程式的的任務型別,程式任務型別一般分為 CPU 密集型任務和 IO 密集型任務,這兩種任務有各自的特點,對程式的要求是不一樣的需要分開對待。

CPU密集型任務

一個程式任務大部分是計算類的,比如邏輯處理、數值比較和計算,我們就稱它是 CPU 密集型任務或計算密集型任務。CPU 密集型任務的特點是要進行大量的計算,消耗 CPU 資源,比如計算圓周率、視訊編解碼這些靠的是 CPU 的運算能力。

CPU 密集型任務雖然也可以用多工完成,但是任務越多,任務之間切換的時間就越多,CPU 執行效率反而更低,所以要最高效地利用 CPU,任務並行數應當等於 CPU 的核心數,避免任務在 CPU 核之間頻繁切換。

晶片線路

IO密集型任務

一個程式涉及到大量網路、磁碟等比較耗時的輸入輸出任務,就稱它是 IO 密集型任務,這類任務的特點是 CPU 消耗很少,任務的大部分時間都在等待 IO 操作完成(因為 IO 的速度遠遠低於 CPU 和記憶體的速度,不是一個數量級的)。

對於 IO 密集型任務,任務越多 CPU 效率越高,但也不是無限的開啟多工,如果任務過多頻繁切換的開銷也不可忽視。常見的大部分程式都是執行 IO 密集型任務,比如網際網路業務的 Web 服務,資料庫操作等。

五彩的乙太網口

服務模型

不管是 CPU 密集型任務還是 IO 密集型任務,要提高伺服器處理能力,可以從軟體和硬體兩個層面來做文章。

先說軟體層面,單個任務處理能力有限,可以通過啟動多個功能完全相同的服務例項,藉此來提高服務整體處理效能,多服務例項的實現主流的技術有三種:多程序、多執行緒、多協程。當然除了用多例項的方式,還有 IO 多路複用、非同步 IO 等技術,為了文章主題明確,不在本文展開討論。

服務模型哪家強

既然有三種技術實現,那麼你可能會問,在三個模型裡選一個最好的來實現服務,該如何選擇一個適合的服務模型呢?

抱歉,小孩子才做選擇我全都要!哈哈,開個玩笑。

答案是沒有最好,服務模型選擇要結合自身服務處理的任務型別。任務型別就是我們上面說的 CPU 密集型和 IO 密集型,只有清楚的知道所處理業務的任務型別,才能在上述服務模型中選擇其一或多種模型組合,來搭建適合你的高效能服務框架。

多程序服務模型

程序概念

程式是一些儲存在磁碟上的指令的有序集合,是靜態的。程序是程式執行的過程,包括了動態建立、排程和消亡的整個過程,程序是程式資源管理的最小單位。

多程序模型

多程序模型是啟動多個服務程序。原來由一個程序做的事,當一個程序忙不過來,建立幾個功能一樣的程序來幫它一起幹活,人多力量大。

由於多程序地址空間不同,資料不能共享,一個程序內建立的變數在另一個程序是無法訪問。作業系統看不下去了,憑什麼同在一臺機器,彼此相愛的兩個程序不能說說話呢?

於是作業系統提供了各種系統呼叫,搭建起各個程序間通訊的橋樑,這些方法統稱為程序間通訊 IPC (IPC InterProcess Communication)

常見程序間通訊方式

管道 Pipe

管道的實質是一個核心緩衝區,程序以先進先出 FIFO 的方式從緩衝區存取資料。 是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係(父子程序間)的程序間通訊。

管道工作原理

  1. 管道一端的程序順序的將資料寫入緩衝區,另一端的程序則順序的讀出資料。

  2. 緩衝區可以看做是一個迴圈佇列,一個數據只能被讀一次,讀出來後在緩衝區就不復存在了。

  3. 當緩衝區為讀空或寫滿,讀資料的程序或寫資料程序進入等待佇列。

  4. 空的緩衝區有新資料寫入,或者滿的緩衝區有資料讀出時,喚醒等待佇列中的程序繼續讀寫。

管道圖解

命名管道 FIFO

上面介紹的管道也稱為匿名管道,只能用於親緣關係的程序間通訊。為了克服這個缺點,出現了有名管道 FIFO 。有名管道提供了一個路徑名與之關聯,以檔案形式存在於檔案系統中,這樣即使不存在親緣關係的程序,只要可以訪問該路徑也能相互通訊。

命名管道支援同一臺計算機的不同程序之間,可靠的、單向或雙向的資料通訊。

訊號 Signal

訊號是Linux系統中用於程序間互相通訊或者操作的一種機制,訊號可以在任何時候發給某一程序,無需知道該程序的狀態。如果該程序當前不是執行態,核心會暫時儲存訊號,當程序恢復執行後傳遞給它。

如果一個訊號被程序設定為阻塞,則該訊號的傳遞被延遲,直到其阻塞被取消是才被傳遞給程序。

訊號在使用者空間程序和核心之間直接互動,核心可以利用訊號來通知使用者空間的程序發生了哪些系統事件,訊號事件主要有兩個來源:

  • 硬體來源:使用者按鍵輸入Ctrl+C退出、硬體異常如無效的儲存訪問等。
  • 軟體終止:終止程序訊號、其他程序呼叫 kill 函式、軟體異常產生訊號。

訊息佇列 Message Queue

訊息佇列是存放在核心中的訊息連結串列,每個訊息佇列由訊息佇列識別符號表示, 只有在核心重啟或主動刪除時,該訊息佇列才會被刪除。

訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。 另外,某個程序往一個訊息佇列寫入訊息之前,並不需要另外讀程序在該佇列上等待訊息的到達。

共享記憶體 Shared memory

共享記憶體是一個程序把地址空間的一段,對映到能被其他程序所訪問的記憶體,一個程序建立、多個程序可訪問,程序就可以直接讀寫這一塊記憶體而不需要進行資料的拷貝,從而大大提高效率。

共享記憶體使得多個程序可以可以直接讀寫同一塊記憶體空間,是最快的可用 IPC 形式,是針對其他通訊機制執行效率較低而設計的。共享記憶體往往與其他通訊機制,如訊號量配合使用,來實現程序間的同步和互斥通訊。

共享記憶體

套接字 Socket

套接字你可能沒聽過這個名字,但絕對是接觸的最多的一種程序間通訊方式。因為我們熟悉的 TCP/IP 協議棧,也是建立在 socket 通訊之上,TCP/IP 構建起了當前的網際網路通訊網路。

它是一種通訊機制,憑藉這種機制,既可以在本機程序間通訊,也可以跨網路通過,因為,套接字通過網路介面將資料傳送到本機的不同程序或遠端計算機的程序。

socket套接字

多執行緒服務模型

執行緒概念

執行緒是操作作業系統能夠進行運算排程的最小單位。執行緒被包含在程序之中,是程序中的實際運作單位,一個程序內可以包含多個執行緒,執行緒是資源排程的最小單位。

多執行緒模型

啟動多個相同功能的程序能提高服務處理能力,但由於各個程序的地址空間相互隔離,通訊不便。

於是,多執行緒服務模型出場。通過前面的學習我們知道,一個程序內的多個執行緒可以共享程序的全部系統資源。程序內建立的多個執行緒都可以訪問程序內的全域性變數。

當然沒有免費的午餐,執行緒雖然能方便的訪問程序資源,但也帶來了額外的問題。比如多執行緒訪公共資源帶來的同步與互斥問題,不同執行緒訪問資源的先後順序會相互影響,如果不做好同步和互斥會產生預期之外的結果,甚至死鎖。

什麼是多執行緒同步

多執行緒同步是執行緒之間的一種直接制約關係,一個執行緒的執行依賴另一個執行緒的通知,當它沒有得到另一個執行緒的通知時必須等待,直到訊息到達時才被喚醒,即有很強的執行先後關係。

比如你搭建了一個商城服務。這個服務的下單流程是這樣的:第一步必須要先挑選商品加入購物車,第二步才能結賬計算訂單金額,假設這兩個步驟的操作分別由兩個執行緒去完成,則這兩個執行緒的操作順序很重要,必須是先下單再結賬,這就是執行緒同步。

什麼是多執行緒互斥

多執行緒互斥指的是多執行緒對資源訪問的排他性。所謂排他性,就是當有多個執行緒都要使用某一共享資源時,任何時刻最多隻允許一個執行緒獲得對這個共享資源的使用權,當共享資源被其中一個執行緒佔有時,其他未獲得資源的執行緒必須等待,直到佔用資源的執行緒釋放資源。

打個比方,你們班只有一臺投影儀,當一個同學在上面放電影的時候,如果老師進來上課要用這個投影儀,那就只能由這個同學放棄投影儀的使用權,交給老師上課投影使用,對,教室裡唯一的投影儀是共享資源,具有排他性,老師和學生比作是兩個執行緒的話,那這兩個執行緒是互斥的訪問共享資源(投影儀)。

投影儀

多執行緒同步和互斥方法

Linux 系統提供以下幾種方法來解決多執行緒的同步和互斥問題,分別是:互斥鎖、條件變數、讀寫鎖、自旋鎖、條件變數。

互斥鎖(同步)

互斥鎖的作用是對臨界區加以保護,以使任意時刻只有一個執行緒能夠執行臨界區的程式碼,實現了多執行緒對臨界資源的互斥訪問。

互斥鎖介面函式:

互斥鎖api

條件變數(同步)

條件變數是用來等待而不是用來上鎖的。條件變數用來自動阻塞一個執行緒,直到某特殊情況發生為止。適合多個執行緒等待某個條件的發生,不使用條件變數,那麼每個執行緒就不斷嘗試互斥鎖並檢測條件是否發生,浪費系統資源。

通常條件變數和互斥鎖同時使用。條件的檢測是在互斥鎖的保護下進行的。如果一個條件為假,一個執行緒自動阻塞,並釋放等待狀態改變的互斥鎖。如果另一個執行緒改變了條件,它發訊號給關聯的條件變數,喚醒一個或多個等待它的執行緒,重新獲得互斥鎖,重新評價條件,可以用來實現執行緒間的同步。

條件變數系統 API 如下:

條件變數API

讀寫鎖(同步)

互斥量要麼是加鎖狀態,要麼是不加鎖狀態,而且一次只有一個執行緒對其進行加鎖。讀寫鎖可以有3種狀態:讀加鎖狀態、寫加鎖狀態和不加鎖狀態。

一次只有一個執行緒可以佔有寫模式讀寫鎖,但是可以有多個執行緒同時佔有讀模式的讀寫鎖。因此,讀寫鎖適合於對資料結構的讀次數比寫次數多得多的情況,且讀寫鎖比互斥量具有更高的並行性。

讀寫鎖加鎖規則

1:如果某執行緒申請了讀鎖,其它執行緒可以再申請讀鎖,但不能申請寫鎖;

2:如果某執行緒申請了寫鎖,其它執行緒不能申請讀鎖,也不能申請寫鎖。

讀寫鎖系統 API

讀寫鎖API

自旋鎖(同步)

互斥鎖得不到鎖時,執行緒會進入休眠,引發任務上下文切換,任務切換涉及一系列耗時的操作,因此用互斥鎖一旦遇到阻塞切換代價是十分昂貴的。

而自旋鎖阻塞後不會引發上下文切換,當鎖被其他執行緒佔有時,獲取鎖的執行緒便會進入自旋,不斷檢測自旋鎖的狀態,直到得到鎖,所謂的自旋就是迴圈等待的意思。

自旋鎖在使用者態使用的比較少,在核心使用的比較多。自旋鎖適用於臨界區程式碼比較短,鎖的持有時間比較短的場景,否則會讓其他執行緒一直等待造成飢餓現象。

自旋鎖 API 介面

自旋鎖API

訊號量(同步與互斥)

訊號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。

訊號量是一個特殊型別的變數,它可以被增加或者減少。可根據操作訊號量值的結果判斷是否對公共資源具有訪問的許可權,當訊號量值大於 0 時,則可以訪問,否則將阻塞。但對其的訪問被保證是原子操作,即使在一個多執行緒程式中也是如此。

訊號量型別:

  • 二進位制訊號量,它只有0和1兩種取值。適用於臨界程式碼每次只能被一個執行執行緒執行,就要用到二進位制訊號量。

  • 計數訊號量。它可以有更大的取值範圍,適用於臨界程式碼允許有限數目的執行緒執行,就需要用到計數訊號量。

訊號量 API

訊號量API

協程服務模型

什麼是協程

什麼是協程呢?協程 Coroutines 是一種比執行緒更加輕量級的微執行緒。類比一個程序可以擁有多個執行緒,一個執行緒也可以擁有多個協程,因此協程又稱微執行緒和纖程。

協程圖解

可以粗略的把協程理解成子程式呼叫,每個子程式都可以在一個單獨的協程內執行。

協程子程式模型

協程服務模型

為了說明什麼是協程模型,先用多執行緒下的生產者消費者模型舉個栗子。

啟動兩個執行緒分別執行兩個函式 Do_some_IODo_some_process ,第一個做耗時的 IO 處理操作,第二個對 IO 操作結果做快速的處理計算工作。虛擬碼如下:

函式虛擬碼

多執行緒執行過程是這樣的:

  1. 生產者執行緒先呼叫函式 Do_some_IO 做比較耗時的 IO 操作,比如從網路套接字中讀取資料這類操作。

  2. 在生產者執行緒執行 Do_some_IO 完成資料讀取之前,消費者執行緒要阻塞等待。

  3. 在消費者執行緒執行 Do_some_process 完成資料處理完成之前,生產者執行緒要阻塞等待。

  4. 在消費者執行緒執行 Do_some_process 完成資料處理完成之後,要通知生成者執行緒繼續 Do_some_IO

可以看到,多執行緒模型為了保證各個執行緒並行工作,需要額外做很多執行緒間的同步和通知工作,而且執行緒頻繁的在阻塞和喚醒間切換,我們知道 Linux 下執行緒是輕量級執行緒 LWP ,每次執行緒切換涉及使用者態和核心態的切換,還是很消耗效能的。

同樣的場景在協程模型裡是怎麼處理的呢?還是用前面的例子,說明協程模型的執行流程。

Do_some_IO()       // IO處理協程
Do_some_process()  // 計算處理協程
  1. 分配生產者協程執行 Do_some_IO 做 IO 處理操作,分配消費者協程執行 Do_some_process 計算處理操作。
  2. 在生產者協程工作期間,消費者協程保持等待。
  3. 當生產者協程完成 IO 處理,返回處理結果給消費者,並把程式執行許可權交給消費者協程向下執行。
協程執行時間線.png

協程優勢

  • 由於協程線上程內實現,因此始終都是一個執行緒操作共享資源,所以不存在多執行緒搶佔資源和資源同步問題。

  • 生產者協程和消費者協程,互相配合協作完成工作,而不是相互搶佔,而且協程建立和切換的開銷比執行緒小得多。

硬體提升效能

前面講的多執行緒、多程序、協程都還只是軟體層面的提高服務處理能力。真正硬核的是從硬體層面提高處理能力,增加 CPU 物理核心數目,當然硬體都是有成本的,所以只有軟體層面已經充分榨乾效能才會考慮增加硬體。

不過,老闆有錢買最好最貴的伺服器另說,這是人民幣玩家和窮逼玩家的區別了,軟體工程師留下了貧困的淚水。

增加機器核心數

CPU領域有一條摩爾定律:大概 18 個月會將晶片的效能提高一倍。現在這個定律變的越來越難以突破,CPU 電晶體密度工作頻率很難再提高,轉而通過增加 CPU 核心數目的方式提高處理器效能。

cpu

目前商用伺服器架構基本都是多核處理器,多核的處理器能夠真正做到程式並行執行,處理效率大幅度提升,那該如何檢視 CPU 核心數目呢?

對於 Windows 作業系統,開啟工作管理員,通過介面的「核心」和「邏輯處理器」能看到。

windows 檢視核心

檢視 cpu 核心數

對於 Linux 作業系統,通過下面 2 種方式檢視 CPU 核心相關資訊。

1. 通過cpuinfo檔案檢視

使用cat /proc/cpuinfo檢視 cpu 核心資訊,如下兩個資訊:

  • processor,指明第幾個cpu處理器
  • cpu cores,指明每個處理器的核心數

cpuinfo 輸出示例:

cpuinfo

2. 通過程式設計介面檢視

除了上面以檔案的形式檢視 cpu 核心資訊之外,系統還提供了程式設計介面可以查詢,系統 API 如下。

檢視核數API

CPU親和性

CPU 親和性是繫結某一程序或執行緒到特定的 CPU 或 CPU 集合,從而使得該程序或執行緒只能被排程執行在繫結的 CPU或 CPU 集合上。

為什麼要設定 CPU 親和性繫結 CPU 呢?理論上程序上一次執行後的上下文資訊會保留在 CPU 的快取中,如果下一次仍然將該程序排程到同一個 CPU 上,就能避免快取未命中對 CPU 處理效能的影響,從而使得程序的執行更加高效。

假如某些程序或執行緒是 CPU 密集型的,不希望被頻繁排程,又或者你有其他特殊需求,不希望程序或執行緒被排程在不同 CPU 之間頻繁切換,則可以將該程序或執行緒繫結到特定的 CPU 上 ,可以在特定場景下優化程式效能。

繫結程序

在多程序模型中,繫結程序到特定的核心,下面是繫結程序的系統 API

繫結執行緒

在多執行緒模型中,繫結執行緒到特定的核心,下面是繫結執行緒的系統 API

設定執行緒親和性

總總結結

本文從程式任務型別出發,區分任務為 CPU 密集型和 IO 密集型兩大類。接著分別說明提高基於這兩類任務的服務效能方法,分為軟體層面的方法和硬體層面的方法,其中軟體層面主要講述利用多程序、多執行緒以及協程模型,當然現有的技術還有 IO 多路複用、非同步 IO 、池化技術等方案。講到多執行緒和多程序,順勢說明了程序間通訊和執行緒間同步互斥技術。

第二部分,講解了從硬體層面提高服務效能:提高機器核心數,並教你如何檢視 CPU 核心數的方法。最後,還可以通過軟硬結合的方式,把硬體核心繫結到指定程序或者執行緒執行,最大程度的利用 CPU 效能。

希望通過本文的學習,讀者對高效能服務模型有個初步的瞭解,並能對服務優化的方法和利弊舉例一二,就是本文的價值所在。

再聊兩句(求個三連)

感謝各位的閱讀,文章的目的是分享對知識的理解,技術類文章我都會反覆求證以求最大程度保證準確性,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。

如果覺得文章寫的還行,對你有所幫助,不要白票 lemon,動動手指「點贊」「三連」是對我持續創作的最大支援。

今天的技術分享就到這裡,我們下期再見。

可以微信搜尋公眾號「 後端技術學堂 」回覆「資料」「1024」有我給你準備的各種程式設計學習資料。文章每週持續更新,我們下期見!

![公眾號二維碼.png](https://i.loli.net/2020/08/01/O3sim8uazGlB