帶你玩轉(zhuǎn)-進(jìn)程并發(fā)與同步!(趕快學(xué)習(xí)起來吧~)
并發(fā) 是指在某一時(shí)間段內(nèi)能夠處理多個(gè)任務(wù)的能力,而 并行 是指同一時(shí)間能夠處理多個(gè)任務(wù)的能力。并發(fā)和并行看起來很像,但實(shí)際上是有區(qū)別的,如下圖(圖片來源于網(wǎng)絡(luò)):

上圖的意思是,有兩條在排隊(duì)買咖啡的隊(duì)列,并發(fā)只有一架咖啡機(jī)在處理,而并行就有兩架的咖啡機(jī)在處理??Х葯C(jī)的數(shù)量越多,并行能力就越強(qiáng)。
可以把上面的兩條隊(duì)列看成兩個(gè)進(jìn)程,并發(fā)就是指只有單個(gè)CPU在處理,而并行就有兩個(gè)CPU在處理。為了讓兩個(gè)進(jìn)程在單核CPU中也能得到執(zhí)行,一般的做法就是讓每個(gè)進(jìn)程交替執(zhí)行一段時(shí)間,比如讓每個(gè)進(jìn)程固定執(zhí)行 100毫秒,執(zhí)行時(shí)間使用完后切換到其他進(jìn)程執(zhí)行。而并行就沒有這種問題,因?yàn)橛袃蓚€(gè)CPU,所以兩個(gè)進(jìn)程可以同時(shí)執(zhí)行。如下圖:

原子操作
上面介紹過,并發(fā)有可能會(huì)打斷當(dāng)前執(zhí)行的進(jìn)程,然后替切換成其他進(jìn)程執(zhí)行。如果有兩個(gè)進(jìn)程同時(shí)對(duì)一個(gè)共享變量 count 進(jìn)行加一操作,由于C語言的 count++ 操作會(huì)被翻譯成如下指令:
那么在并發(fā)的情況下,有可能出現(xiàn)如下問題:

假設(shè)count變量初始值為0:
進(jìn)程1執(zhí)行完 mov eax, [count] 后,寄存器eax內(nèi)保存了count的值0。
進(jìn)程2被調(diào)度執(zhí)行。進(jìn)程2執(zhí)行 count++ 的所有指令,將累加后的count值1寫回到內(nèi)存。
進(jìn)程1再次被調(diào)度執(zhí)行,計(jì)算count的累加值仍為1,寫回到內(nèi)存。
雖然進(jìn)程1和進(jìn)程2執(zhí)行了兩次 count++ 操作,但是count最后的值為1,而不是2。
要解決這個(gè)問題就需要使用 原子操作,原子操作是指不能被打斷的操作,在單核CPU中,一條指令就是原子操作。比如上面的問題可以把 count++ 語句翻譯成指令 inc [count] 即可。Linux也提供了這樣的原子操作,如對(duì)整數(shù)加一操作的 atomic_inc():
在多核CPU中,一條指令也不一定是原子操作,比如 inc [count] 指令在多核CPU中需要進(jìn)行如下過程:
從內(nèi)存將count的數(shù)據(jù)讀取到cpu。
累加讀取的值。
將修改的值寫回count內(nèi)存。
Intel x86 CPU 提供了 lock 前綴來鎖住總線,可以讓指令保證不被其他CPU中斷,如下:
鎖
原子操作 能夠保證操作不被其他進(jìn)程干擾,但有時(shí)候一個(gè)復(fù)雜的操作需要由多條指令來實(shí)現(xiàn),那么就不能使用原子操作了,這時(shí)候可以使用 鎖 來實(shí)現(xiàn)。
計(jì)算機(jī)科學(xué)中的 鎖 與日常生活的 鎖 有點(diǎn)類似,舉個(gè)例子:比如要上公廁,首先找到一個(gè)沒有人的廁所,然后把廁所門鎖上。其他人要使用的話,必須等待當(dāng)前這人使用完畢,并且把門鎖打開才能使用。在計(jì)算機(jī)中,要對(duì)某個(gè)公共資源進(jìn)行操作時(shí),必須對(duì)公共資源進(jìn)行上鎖,然后才能使用。如果不上鎖,那么就可能導(dǎo)致數(shù)據(jù)混亂的情況。
在Linux內(nèi)核中,比較常用的鎖有:自旋鎖、信號(hào)量、讀寫鎖 等,下面介紹一下自旋鎖和信號(hào)量的實(shí)現(xiàn)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


自旋鎖
自旋鎖 只能在多核CPU系統(tǒng)中,其核心原理是 原子操作,原理如下圖:

使用 自旋鎖 時(shí),必須先對(duì)自旋鎖進(jìn)行初始化(設(shè)置為1),上鎖過程如下:
對(duì)自旋鎖 lock 進(jìn)行減一操作,判斷結(jié)果是否等于0,如果是表示上鎖成功并返回。
如果不等于0,表示其他進(jìn)程已經(jīng)上鎖,此時(shí)必須不斷比較自旋鎖 lock 的值是否等于1(表示已經(jīng)解鎖)。
如果自旋鎖 lock 等于1,跳轉(zhuǎn)到第一步繼續(xù)進(jìn)行上鎖操作。
由于Linux的自旋鎖使用匯編實(shí)現(xiàn),所以比較苦澀難懂,這里使用C語言來模擬一下:
上面代碼將 result = --(*lock); 當(dāng)成原子操作,解鎖過程只需要把 lock 設(shè)置為1即可。由于自旋鎖會(huì)不斷嘗試上鎖操作,并不會(huì)對(duì)進(jìn)程進(jìn)行調(diào)度,所以在單核CPU中可能會(huì)導(dǎo)致 100% 的CPU占用率。另外,自旋鎖只適合粒度比較小的操作,如果操作粒度比較大,就需要使用信號(hào)量這種可調(diào)度進(jìn)程的鎖。
信號(hào)量
與 自旋鎖 不一樣,當(dāng)當(dāng)前進(jìn)程對(duì) 信號(hào)量 進(jìn)行上鎖時(shí),如果其他進(jìn)程已經(jīng)對(duì)其進(jìn)行上鎖,那么當(dāng)前進(jìn)程會(huì)進(jìn)入睡眠狀態(tài),等待其他進(jìn)程對(duì)信號(hào)量進(jìn)行解鎖。過程如下圖:

在Linux內(nèi)核中,信號(hào)量使用 struct semaphore 表示,定義如下:
各個(gè)字段的作用如下:
lock:自旋鎖,用于對(duì)多核CPU平臺(tái)進(jìn)行同步。
count:信號(hào)量的計(jì)數(shù)器,上鎖時(shí)對(duì)其進(jìn)行減一操作(count--),如果得到的結(jié)果為大于等于0,表示成功上鎖,如果小于0表示已經(jīng)被其他進(jìn)程上鎖。
wait_list:正在等待信號(hào)量解鎖的進(jìn)程隊(duì)列。
信號(hào)量 上鎖通過 down() 函數(shù)實(shí)現(xiàn),代碼如下:
上面代碼可以看出,down() 函數(shù)首先對(duì)信號(hào)量進(jìn)行自旋鎖操作(為了避免多核CPU競爭),然后比較計(jì)數(shù)器是否大于0,如果是對(duì)計(jì)數(shù)器進(jìn)行減一操作,并且返回,否則調(diào)用 __down() 函數(shù)進(jìn)行下一步操作。__down() 函數(shù)實(shí)現(xiàn)如下:
__down() 函數(shù)最終調(diào)用 __down_common() 函數(shù),而 __down_common() 函數(shù)的操作過程如下:
把當(dāng)前進(jìn)程添加到信號(hào)量的等待隊(duì)列中。
切換到其他進(jìn)程運(yùn)行,直到被其他進(jìn)程喚醒。
如果當(dāng)前進(jìn)程獲得信號(hào)量鎖(由解鎖進(jìn)程傳遞),那么函數(shù)返回。
接下來看看解鎖過程,解鎖過程主要通過 up() 函數(shù)實(shí)現(xiàn),代碼如下:
解鎖過程如下:
判斷當(dāng)前信號(hào)量是否有等待的進(jìn)程,如果沒有等待的進(jìn)程, 直接對(duì)計(jì)數(shù)器加一操作
如果有等待的進(jìn)程,那么獲取到等待隊(duì)列的第一個(gè)進(jìn)程。
把進(jìn)程從等待隊(duì)列中刪除。
告訴進(jìn)程已經(jīng)獲得信號(hào)量鎖
喚醒進(jìn)程。
