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 。有名管道提供了一個路徑名與之關聯,以文件形式存在於文件系統中,這樣即使不存在親緣關係的進程,只要可以訪問該路徑也能相互通信。

命名管道支持同一臺計算機的不同進程之間,可靠的、單向或雙向的數據通信。
FIFO圖解.png

信號 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
    多線程執行模型.png

可以看到,多線程模型爲了保證各個線程並行工作,需要額外做很多線程間的同步和通知工作,而且線程頻繁的在阻塞和喚醒間切換,我們知道 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」有我給你準備的各種編程學習資料。文章每週持續更新,我們下期見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章