1. 程式人生 > >深入理解併發/並行,阻塞/非阻塞,同步/非同步

深入理解併發/並行,阻塞/非阻塞,同步/非同步

1. 阻塞,非阻塞

首先,阻塞這個詞來自作業系統的執行緒/程序的狀態模型中,如下圖:

這裡寫圖片描述

一個執行緒/程序經歷的5個狀態,建立,就緒,執行,阻塞,終止。各個狀態的轉換條件如上圖,其中有個阻塞狀態,就是說當執行緒中呼叫某個函式,需要IO請求,或者暫時得不到競爭資源的,作業系統會把該執行緒阻塞起來,避免浪費CPU資源,等到得到了資源,再變成就緒狀態,等待CPU排程執行。

阻塞呼叫是指呼叫結果返回之前,呼叫者會進入阻塞狀態等待。只有在得到結果之後才會返回。

非阻塞呼叫是指在不能立刻得到結果之前,該函式不會阻塞當前執行緒,而會立刻返回。

阻塞呼叫:比如 socket 的 recv(),呼叫這個函式的執行緒如果沒有資料返回,它會一直阻塞著,也就是 recv() 後面的程式碼都不會執行了,程式就停在 recv() 這裡等待,所以一般把 recv() 放在單獨的執行緒裡呼叫。

非阻塞呼叫:比如非阻塞socket 的 send(),呼叫這個函式,它只是把待發送的資料複製到TCP輸出緩衝區中,就立刻返回了,執行緒並不會阻塞,資料有沒有發出去 send() 是不知道的,不會等待它發出去才返回的。

拓展

如果執行緒始終阻塞著,永遠得不到資源,於是就發生了死鎖。

比如A執行緒要X,Y資源才能繼續執行,B執行緒也要X,Y資源才能執行,但X,Y同時只能給一個執行緒用(即互斥條件)用的時候其他執行緒又不能搶奪。

A 有 X,等待 Y。
B 有 Y,等待 X。

於是A,B發生了迴圈等待,造成死鎖。給使用者的感覺就是程式卡著不動了。

在寫程式碼的時候要特別注意共享資源的使用,用訊號量控制好,避免造成死鎖。死鎖的解除有個著名的銀行家演算法

阻塞和掛起:阻塞是被動的,比如搶不到資源。掛起是主動的,執行緒自己呼叫 suspend() 把自己退出執行態了,某些時候呼叫 resume() 又恢復執行。

執行緒執行完就會被銷燬,如果不想執行緒被頻繁的建立,銷燬,怎麼辦?可以給執行緒裡面寫個死迴圈,或者讓執行緒有任務的時候執行,沒任務的時候掛起,就像iOS中的 runloop 機制一樣。執行緒就不會隨便的終止了。

2. 同步,非同步

同步:在發出一個同步呼叫時,在沒有得到結果之前,該呼叫就不返回。

非同步:在發出一個非同步呼叫後,呼叫者不會立刻得到結果,該呼叫就返回了。

同步例子

int n = func();
next
(); // func() 的結果沒有返回,next() 就不會執行,直到 func() 執行完。

非同步例子

func(callback);
next();
...

void callback(int n)     // func 結果回撥
{
  int k = n;
}
// func() 執行後,還沒得出結果就立即返回,然後執行 next() 了
// 等到結果出來,func() 回撥 callback() 通知呼叫者結果。

同步的定義看起來跟阻塞很像,但是同步跟阻塞是兩個概念,同步呼叫的時候,執行緒不一定阻塞,呼叫雖然沒返回,但它還是在執行狀態中的,CPU很可能還在執行這段程式碼,而阻塞的話,它就肯定不在CPU中跑這個程式碼了。這就是同步和阻塞的區別。同步是肯定可以在,阻塞是肯定不在。

非同步和非阻塞的定義比較像,兩者的區別是非同步是說呼叫的時候結果不會馬上返回,執行緒可能被阻塞起來,也可能不阻塞,兩者沒關係。非阻塞是說呼叫的時候,執行緒肯定不會進入阻塞狀態。

上面兩組概念,就有4種組合。

同步阻塞呼叫:得不到結果不返回,執行緒進入阻塞態等待。

同步非阻塞呼叫:得不到結果不返回,執行緒不阻塞一直在CPU執行。

非同步阻塞呼叫:去到別的執行緒,讓別的執行緒阻塞起來等待結果,自己不阻塞。

非同步非阻塞呼叫:去到別的執行緒,別的執行緒一直在執行,直到得出結果。

3. 併發,並行

先從定義說起,定義經過我通俗化了,原定義有點難理解。

併發是指一個時間段內,有幾個程式都在同一個CPU上執行,但任意一個時刻點上只有一個程式在處理機上執行。

並行是指一個時間段內,有幾個程式都在幾個CPU上執行,任意一個時刻點上,有多個程式在同時執行,並且多道程式之間互不干擾。 兩者區別如下圖

這裡寫圖片描述

這裡寫圖片描述

並行是多個程式在多個CPU上同時執行,任意一個時刻可以有很多個程式同時執行,互不干擾。

併發是多個程式在一個CPU上執行,CPU在多個程式之間快速切換,微觀上不是同時執行,任意一個時刻只有一個程式在執行,但巨集觀上看起來就像多個程式同時執行一樣,因為CPU切換速度非常快,時間片是64ms(每64ms切換一次,不同的作業系統有不同的時間),人類的反應速度是100ms,你還沒反應過來,CPU已經切換了好幾個程式了。

舉個例子吧,並行就是,多個人,有人在掃地,有人在做飯,有人在洗衣服,掃地,做飯,洗衣服都是同時進行的。
併發就是,有一個人,這個人一會兒掃地,一會兒做飯,一會兒洗衣服,他在這3件事中來回做,同一時刻只做一件事,不是同時做的,但最後3件事都可以做完。

時間片大小的選取
時間片取的小,假設是20ms,切換耗時假設是 10ms。
那麼使用者感覺不到多個程式之間會卡,響應很快,因為切換太快了,但是CPU的利用率就低了,20 / (20 + 10) = 66% 只有這麼多,33%都浪費了。

時間片取的大,假設是200ms,切換耗時是 10ms
那麼使用者會覺得程式卡,響應慢,因為要200ms後才輪到我的程式執行,但是CPU利用率就高了,200 / (200 + 10) = 95% 有這麼多被利用的。

所以時間片取太大或者太小都不好,一般在 10 - 100 ms 之間。

CPU排程策略

在併發執行中,CPU需要在多個程式之間來回切換,那麼如何切換就有一些策略

3.1 先來先服務 - 時間片輪轉排程

這個很簡單,就是誰先來,就給誰分配時間片執行,缺點是有些緊急的任務要很久才能得到執行。

3.2 優先順序排程

每個執行緒有一個優先順序,CPU每次去拿優先順序高的執行,優先順序低的等等,為了避免等太久,每等一定時間,就給執行緒提高一個優先順序

3.3 最短作業優先

把執行緒任務量排序,每次拿處理時間短的執行緒執行,就像我去銀行辦業務一樣,我的事情很快就處理完了,所以讓我插隊先辦完,後面時間長的人先等等,時間長的人就很難得到響應了。

3.4 最高響應比優先

用執行緒的等待時間除以服務時間,得到響應比,響應比小的優先執行。這樣不會造成某些任務一直得不到響應。

3.5 多級反饋佇列排程

有多個優先順序不同的佇列,每個佇列裡面有多個等待執行緒。
CPU每次從優先順序高的遍歷到低的,取隊首的執行緒執行,執行完了放回隊尾,優先順序越高,時間片越短,即響應越快,時間片就不是固定的了。
佇列內部還是用先來先服務的策略。