一文读懂 | 进程并发与同步
并發(fā)?是指在某一時(shí)間段內(nèi)能夠處理多個(gè)任務(wù)的能力,而?并行?是指同一時(shí)間能夠處理多個(gè)任務(wù)的能力。并發(fā)和并行看起來(lái)很像,但實(shí)際上是有區(qū)別的,如下圖(圖片來(lái)源于網(wǎng)絡(luò)):
concurrency-parallelism上圖的意思是,有兩條在排隊(duì)買咖啡的隊(duì)列,并發(fā)只有一架咖啡機(jī)在處理,而并行就有兩架的咖啡機(jī)在處理。咖啡機(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í)行。而并行就沒(méi)有這種問(wèn)題,因?yàn)橛袃蓚€(gè)CPU,所以兩個(gè)進(jìn)程可以同時(shí)執(zhí)行。如下圖:
concurrency-parallelism原子操作
上面介紹過(guò),并發(fā)有可能會(huì)打斷當(dāng)前執(zhí)行的進(jìn)程,然后替切換成其他進(jìn)程執(zhí)行。如果有兩個(gè)進(jìn)程同時(shí)對(duì)一個(gè)共享變量?count?進(jìn)行加一操作,由于C語(yǔ)言的?count++?操作會(huì)被翻譯成如下指令:
mov eax, [count] inc eax mov [count], eax那么在并發(fā)的情況下,有可能出現(xiàn)如下問(wèn)題:
concurrency-problem假設(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è)問(wèn)題就需要使用?原子操作,原子操作是指不能被打斷的操作,在單核CPU中,一條指令就是原子操作。比如上面的問(wèn)題可以把?count++?語(yǔ)句翻譯成指令?inc [count]?即可。Linux也提供了這樣的原子操作,如對(duì)整數(shù)加一操作的?atomic_inc():
static?__inline__?void?atomic_inc(atomic_t?*v) {__asm__?__volatile__(LOCK?"incl?%0":"=m"?(v->counter):"m"?(v->counter)); }在多核CPU中,一條指令也不一定是原子操作,比如?inc [count]?指令在多核CPU中需要進(jìn)行如下過(guò)程:
從內(nèi)存將count的數(shù)據(jù)讀取到cpu。
累加讀取的值。
將修改的值寫回count內(nèi)存。
Intel x86 CPU?提供了?lock?前綴來(lái)鎖住總線,可以讓指令保證不被其他CPU中斷,如下:
lock inc [count]鎖
原子操作?能夠保證操作不被其他進(jìn)程干擾,但有時(shí)候一個(gè)復(fù)雜的操作需要由多條指令來(lái)實(shí)現(xiàn),那么就不能使用原子操作了,這時(shí)候可以使用?鎖?來(lái)實(shí)現(xiàn)。
計(jì)算機(jī)科學(xué)中的?鎖?與日常生活的?鎖?有點(diǎn)類似,舉個(gè)例子:比如要上公廁,首先找到一個(gè)沒(méi)有人的廁所,然后把廁所門鎖上。其他人要使用的話,必須等待當(dāng)前這人使用完畢,并且把門鎖打開(kāi)才能使用。在計(jì)算機(jī)中,要對(duì)某個(gè)公共資源進(jìn)行操作時(shí),必須對(duì)公共資源進(jìn)行上鎖,然后才能使用。如果不上鎖,那么就可能導(dǎo)致數(shù)據(jù)混亂的情況。
在Linux內(nèi)核中,比較常用的鎖有:自旋鎖、信號(hào)量、讀寫鎖?等,下面介紹一下自旋鎖和信號(hào)量的實(shí)現(xiàn)。
自旋鎖
自旋鎖?只能在多核CPU系統(tǒng)中,其核心原理是?原子操作,原理如下圖:
spinlock使用?自旋鎖?時(shí),必須先對(duì)自旋鎖進(jìn)行初始化(設(shè)置為1),上鎖過(guò)程如下:
對(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語(yǔ)言來(lái)模擬一下:
void?spin_lock(amtoic_t?*lock) { again:result?=?--(*lock);if?(result?==?0)?{return;}while?(true)?{if?(*lock?==?1)?{goto?again;}} }上面代碼將?result = --(*lock);?當(dāng)成原子操作,解鎖過(guò)程只需要把?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)行解鎖。過(guò)程如下圖:
semaphore在Linux內(nèi)核中,信號(hào)量使用?struct semaphore?表示,定義如下:
struct?semaphore?{raw_spinlock_t??????lock;unsigned?int????????count;struct?list_head????wait_list; };各個(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)量?上鎖通過(guò)?down()?函數(shù)實(shí)現(xiàn),代碼如下:
void?down(struct?semaphore?*sem) {unsigned?long?flags;spin_lock_irqsave(&sem->lock,?flags);if?(likely(sem->count?>?0))sem->count--;else__down(sem);spin_unlock_irqrestore(&sem->lock,?flags); }上面代碼可以看出,down()?函數(shù)首先對(duì)信號(hào)量進(jìn)行自旋鎖操作(為了避免多核CPU競(jìng)爭(zhēng)),然后比較計(jì)數(shù)器是否大于0,如果是對(duì)計(jì)數(shù)器進(jìn)行減一操作,并且返回,否則調(diào)用?__down()?函數(shù)進(jìn)行下一步操作。__down()?函數(shù)實(shí)現(xiàn)如下:
static?noinline?void?__sched?__down(struct?semaphore?*sem) {__down_common(sem,?TASK_UNINTERRUPTIBLE,?MAX_SCHEDULE_TIMEOUT); }static?inline?int?__down_common(struct?semaphore?*sem,long?state,?long?timeout) {struct?task_struct?*task?=?current;struct?semaphore_waiter?waiter;//?把當(dāng)前進(jìn)程添加到等待隊(duì)列中l(wèi)ist_add_tail(&waiter.list,?&sem->wait_list);waiter.task?=?task;waiter.up?=?0;for?(;;)?{...__set_task_state(task,?state);spin_unlock_irq(&sem->lock);timeout?=?schedule_timeout(timeout);spin_lock_irq(&sem->lock);if?(waiter.up)?//?當(dāng)前進(jìn)程是否獲得信號(hào)量鎖?return?0;}... }__down()?函數(shù)最終調(diào)用?__down_common()?函數(shù),而?__down_common()?函數(shù)的操作過(guò)程如下:
把當(dāng)前進(jìn)程添加到信號(hào)量的等待隊(duì)列中。
切換到其他進(jìn)程運(yùn)行,直到被其他進(jìn)程喚醒。
如果當(dāng)前進(jìn)程獲得信號(hào)量鎖(由解鎖進(jìn)程傳遞),那么函數(shù)返回。
接下來(lái)看看解鎖過(guò)程,解鎖過(guò)程主要通過(guò)?up()?函數(shù)實(shí)現(xiàn),代碼如下:
void?up(struct?semaphore?*sem) {unsigned?long?flags;raw_spin_lock_irqsave(&sem->lock,?flags);if?(likely(list_empty(&sem->wait_list)))?//?如果沒(méi)有等待的進(jìn)程, 直接對(duì)計(jì)數(shù)器加一操作sem->count++;else__up(sem);?//?如果有等待進(jìn)程,?那么調(diào)用?__up()?函數(shù)進(jìn)行喚醒raw_spin_unlock_irqrestore(&sem->lock,?flags); }static?noinline?void?__sched?__up(struct?semaphore?*sem) {//?獲取到等待隊(duì)列的第一個(gè)進(jìn)程struct?semaphore_waiter?*waiter?=?list_first_entry(&sem->wait_list,?struct?semaphore_waiter,?list);list_del(&waiter->list);???????//?把進(jìn)程從等待隊(duì)列中刪除waiter->up?=?1;????????????????//?告訴進(jìn)程已經(jīng)獲得信號(hào)量鎖wake_up_process(waiter->task);?//?喚醒進(jìn)程 }解鎖過(guò)程如下:
判斷當(dāng)前信號(hào)量是否有等待的進(jìn)程,如果沒(méi)有等待的進(jìn)程, 直接對(duì)計(jì)數(shù)器加一操作
如果有等待的進(jìn)程,那么獲取到等待隊(duì)列的第一個(gè)進(jìn)程。
把進(jìn)程從等待隊(duì)列中刪除。
告訴進(jìn)程已經(jīng)獲得信號(hào)量鎖
喚醒進(jìn)程。
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語(yǔ)言
我的知識(shí)小密圈
關(guān)注公眾號(hào),后臺(tái)回復(fù)「1024」獲取學(xué)習(xí)資料網(wǎng)盤鏈接。
歡迎點(diǎn)贊,關(guān)注,轉(zhuǎn)發(fā),在看,您的每一次鼓勵(lì),我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關(guān)注我的公眾號(hào)
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的一文读懂 | 进程并发与同步的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 100个Python实战项目(十一)如何
- 下一篇: 浪潮ssr服务器安全加固系统贵吗,浪潮S