1. 程式人生 > >我已經理解了併發和並行的區別

我已經理解了併發和並行的區別

理解併發、並行的例子

先舉例子來理解這2個概念的區別。

老師讓兩個同學去辦公室談話。如果這兩同學(程序)是並列跨過辦公室門(CPU)的,那麼就是並行。如果同學A先進同學B後進入(或者先B後A),或者兩人並列同時進入,但是在辦公室外的路人甲(使用者)看來,同學A和同學B同時都在辦公室內,這是併發。

其實這個例子不合理,因為真正的並行是多核CPU下的概念,但上面這個簡單的例子非常有助於理解。

如果舉例要精確一點,那麼大概是這樣的:進辦公室有兩個門(兩CPU),如果兩同學分別從不同的門進入,不管先後性,兩者互相獨立,那麼是並行;如果兩同學不管以什麼方式進入,在路人甲看來,他兩同時都在辦公室內,就是併發。

我不信到現在還不理解併發和並行。

併發和並行的理論性解釋

為什麼作業系統上可以同時執行多個程式而使用者感覺不出來?

這是因為無論是單CPU還是多CPU,作業系統都營造出了可以同時執行多個程式的假象。實際的過程作業系統對程序的排程以及CPU的快速上下文切換實現的:每個程序執行一會就先停下來,然後CPU切換到下個被作業系統排程到的程序上使之執行。因為切換的很快,使得使用者認為作業系統一直在服務自己的程式。

再來解釋併發就容易理解多了。

併發(concurrent)指的是多個程式可以同時執行的現象,更細化的是多程序可以同時執行或者多指令可以同時執行。但這不是重點,在描述併發的時候也不會去扣這種字眼是否精確,併發的重點在於它是一種現象。併發描述的是多程序同時執行的現象。但實際上,對於單核心CPU來說,同一時刻只能執行一個程序。所以,這裡的"同時執行"表示的不是真的同一時刻有多個程序執行的現象,這是並行的概念,而是提供一種功能讓使用者看來多個程式同時執行起來了,但實際上這些程式中的程序不是一直霸佔CPU的,而是執行一會停一會。

所以,併發和並行的區別就很明顯了。它們雖然都說是"多個程序同時執行",但是它們的"同時"不是一個概念。並行的"同時"是同一時刻可以多個程序在執行(處於running),併發的"同時"是經過上下文快速切換,使得看上去多個程序同時都在執行的現象,是一種OS欺騙使用者的現象。

再次註明,併發是一種現象,之所以能有這種現象的存在,和CPU的多少無關,而是和程序排程以及上下文切換有關的。

理解了概念,再來深入擴充套件下。

序列、並行和併發

任務描述

如圖:

任務是將左邊的一堆柴全部搬到右邊燒掉,每個任務包括三個過程:取柴,運柴,放柴燒火。

這三個過程分別對應一個函式:

func get { geting }
func carry { carrying }
func unload { unloading }

序列模式

序列表示所有任務都一一按先後順序進行。序列意味著必須先裝完一車柴才能運送這車柴,只有運送到了,才能卸下這車柴,並且只有完成了這整個三個步驟,才能進行下一個步驟。

和稍後所解釋的並行相對比,序列是一次只能取得一個任務,並執行這個任務。

假設這堆柴需要運送4次才能運完,那麼當寫下的程式碼類似於下面這種時,那麼就是序列非併發的模式:

for(i=0;i<4;i++){
    get()
    carry()
    unload()
}

或者,將三個過程的程式碼全部集中到一個函式中也是如此:

func task {
    geting
    carrying
    unloading
}

for(i=0;i<4;i++){
    task()
}

這兩種都是序列的程式碼模式。畫圖描述:

並行模式

並行意味著可以同時取得多個任務,並同時去執行所取得的這些任務。並行模式相當於將長長的一條佇列,劃分成了多條短佇列,所以並行縮短了任務佇列的長度。

正如前面所舉的兩同學進辦公室的例子,序列的方式下,必須1個同學進入後第二個同學才進入,佇列長度為2,而並行方式下可以同時進入,佇列長度減半了。

並行的效率從程式碼層次上強依賴於多程序/多執行緒程式碼,從硬體角度上則依賴於多核CPU。

對於單程序/單執行緒,由於只有一個程序/執行緒在執行,所以儘管同時執行所取得的多個任務,但實際上這個程序/執行緒是不斷的在多工之間切換,一會執行一下這個,一會執行一下那個,就像是一個人在不同地方之間來回奔波。所以,單程序/執行緒的並行,效率比序列更低。

對於多程序/多執行緒,各程序/執行緒都可以執行各自所取得的任務,這是真正的並行。

但是,還需要考慮硬體層次上CPU核心數,如果只有單核CPU,那麼在硬體角度上這單核CPU一次也只能執行一個任務,上面多程序/多執行緒的並行也並非真正意義上的並行。只有多核CPU,並且多程序/多執行緒並行,才是真正意義上的並行。

如下圖,是多程序/多執行緒(2個工作者)的並行:

併發

並發表示多個任務同時都要執行的現象,更詳細的概念前面已經說面的夠具體了。

其實,很多場景下都會使用併發的概念。比如同時500個http請求湧向了web伺服器,比如有時候說併發數是1000等。

有時候也將併發當成任務,比如500併發數意味著500個任務,表示的是在一個特定的時間段內(約定俗成的大家認為是1秒)可以完成500個任務。這500個任務可以是單程序/單執行緒方式處理的,這時表示的是併發不併行的模式(coroutine就是典型的併發不併行),即先執行完一個任務後才執行另一個任務,也可以是多程序/多執行緒方式處理的,這時表示的是併發且並行模式。

要解決大併發問題,通常是將大任務分解成多個小任務。很典型的一個例子是處理客戶端的請求任務,這個大任務裡面包含了監聽並建立客戶端的連線、處理客戶端的請求、響應客戶端。但基本上所有這類程式,都將這3部分任務分開了:在執行任何一個小任務的時候,都可以通過一些手段使得可以執行其它小任務,比如在處理請求的時候,可以繼續保持監聽狀態。

由於作業系統對程序的排程是隨機的,所以切分成多個小任務後,可能會從任一小任務處執行。這可能會出現一些現象:

  • 可能出現一個小任務執行了多次,還沒開始下個任務的情況。這時一般會採用佇列或類似的資料結構來存放各個小任務的成果。比如負責監聽的程序已經執行了多次,建立了多個連線,但是還沒有排程到處理請求的程序去處理任何一個請求。

  • 可能出現還沒準備好第一步就執行第二步的可能。這時,一般採用多路複用或非同步的方式,比如只有準備好產生了事件通知才執行某個任務。比如還沒有和任何一個客戶端建立連線時,就去執行了處理請求的程序。
  • 可以多程序/多執行緒的方式並行執行這些小任務。也可以單程序/單執行緒執行這些小任務,這時很可能要配合多路複用才能達到較高的效率

看圖非常容易理解:

上圖中將一個任務中的三個步驟取柴、運柴、卸柴劃分成了獨立的小任務,有取柴的老鼠,有運柴的老鼠,有卸柴燒火的老鼠。

如果上圖中所有的老鼠都是同一只,那麼是序列併發的,如果是不同的多隻老鼠,那麼是並行併發的。

總結

並行和序列:

  • 序列:一次只能取得一個任務並執行這一個任務
  • 並行:可以同時通過多程序/多執行緒的方式取得多個任務,並以多程序或多執行緒的方式同時執行這些任務
  • 注意點:
    • 如果是單程序/單執行緒的並行,那麼效率比序列更差
    • 如果只有單核cpu,多程序並行並沒有提高效率
    • 從任務佇列上看,由於同時從佇列中取得多個任務並執行,相當於將一個長任務佇列變成了短佇列

併發:

  • 併發是一種現象:同時執行多個程式或多個任務需要被處理的現象
  • 這些任務可能是並行執行的,也可能是序列執行的,和CPU核心數無關,是作業系統程序排程和CPU上下文切換達到的結果
  • 解決大併發的一個思路是將大任務分解成多個小任務:
    • 可能要使用一些資料結構來避免切分成多個小任務帶來的問題
    • 可以多程序/多執行緒並行的方式去執行這些小任務達到高效率
    • 或者以單程序/單執行緒配合多路複用執行這些小任務來達到高效率