看完了進(jìn)程同步與互斥機(jī)制,我終于徹底理解了 PV 操作
全文脈絡(luò)思維導(dǎo)圖如下:

1. 什么是進(jìn)程同步
在多道批處理系統(tǒng)中,多個(gè)進(jìn)程是可以并發(fā)執(zhí)行的,但由于系統(tǒng)的資源有限,進(jìn)程的執(zhí)行不是一貫到底的, 而是走走停停,以不可預(yù)知的速度向前推進(jìn),這就是進(jìn)程的「異步性」。
那么,「進(jìn)程的異步性會(huì)帶來什么問題呢」?舉個(gè)例子,如果有 A、B 兩個(gè)進(jìn)程分別負(fù)責(zé)讀和寫數(shù)據(jù)的操作,這兩個(gè)線程是相互合作、相互依賴的。那么寫數(shù)據(jù)應(yīng)該發(fā)生在讀數(shù)據(jù)之前。而實(shí)際上,由于異步性的存在,可能會(huì)發(fā)生先讀后寫的情況,而此時(shí)由于緩沖區(qū)還沒有被寫入數(shù)據(jù),讀進(jìn)程 A 沒有數(shù)據(jù)可讀,因此讀進(jìn)程 A 被阻塞。

進(jìn)程同步(synchronization)就是用來解決這個(gè)問題的。從上面的例子我們能看出,一個(gè)進(jìn)程的執(zhí)行可能影響到另一個(gè)進(jìn)程的執(zhí)行,「所謂進(jìn)程同步就是指協(xié)調(diào)這些完成某個(gè)共同任務(wù)的并發(fā)線程,在某些位置上指定線程的先后執(zhí)行次序、傳遞信號或消息」。
再舉個(gè)生活中的進(jìn)程同步的例子,你想要喝熱水,于是你打了一壺水開始燒,在這壺水燒開之前,你只能一直等著,水燒開之后水壺自然會(huì)發(fā)生響聲提醒你來喝水,于是你就可以喝水了。就是說「水燒開這個(gè)事情必須發(fā)生在你喝水之前」。
注意不要把進(jìn)程同步和進(jìn)程調(diào)度搞混了:
進(jìn)程調(diào)度是為了最大程度的利用 CPU 資源,選用合適的算法調(diào)度就緒隊(duì)列中的進(jìn)程。 進(jìn)程同步是為了協(xié)調(diào)一些進(jìn)程以完成某個(gè)任務(wù),比如讀和寫,你肯定先寫后讀,不能先讀后寫吧,這就是進(jìn)程同步做的事情了,指定這些進(jìn)程的先后執(zhí)行次序使得某個(gè)任務(wù)能夠順利完成。
2. 什么是進(jìn)程互斥
同樣的,也是因?yàn)檫M(jìn)程的并發(fā)性,并發(fā)執(zhí)行的線程不可避免地需要共享一些系統(tǒng)資源,比如內(nèi)存、打印機(jī)、攝像頭等。舉個(gè)例子:我們?nèi)W(xué)校打印店打印論文,你按下了 WPS 的 “打印” 選項(xiàng),于是打印機(jī)開始工作。你的論文打印到一半時(shí),另一位同學(xué)按下了 Word 的 “打印” 按鈕,開始打印他自己的論文。想象一下如果兩個(gè)進(jìn)程可以隨意的、并發(fā)的共享打印機(jī)資源,會(huì)發(fā)生什么情況?
顯然,兩個(gè)進(jìn)程并發(fā)運(yùn)行,導(dǎo)致打印機(jī)設(shè)備交替的收到 WPS 和 Word 兩個(gè)進(jìn)程發(fā)來的打印請求,結(jié)果兩篇論文的內(nèi)容混雜在一起了。
進(jìn)程互斥(mutual exclusion)就是用來解決這個(gè)問題的。當(dāng)某個(gè)進(jìn)程 A 在訪問打印機(jī)時(shí),如果另一個(gè)進(jìn)程 B 也想要訪問打印機(jī),它就必須等待,直到 A 進(jìn)程訪問結(jié)束并釋放打印機(jī)資源后,B 進(jìn)程才能去訪問。
實(shí)際上,像上述的打印機(jī)這種「在一個(gè)時(shí)間段內(nèi)只允許一個(gè)進(jìn)程使用的資源」(這也就是互斥的意思),我們將其稱為「臨界資源」,對臨界資源進(jìn)行訪問的那段代碼稱為「臨界區(qū)」。

通俗的對比一下進(jìn)程互斥和進(jìn)程同步:
進(jìn)程同步:進(jìn)程 A 應(yīng)在進(jìn)程 B 之前執(zhí)行 進(jìn)程互斥:進(jìn)程 A 和進(jìn)程 B 不能在同一時(shí)刻執(zhí)行
從上不難看出,「進(jìn)程互斥是一種特殊的進(jìn)程同步」,即逐次使用臨界資源,也是對進(jìn)程使用資源的先后執(zhí)行次序的一種協(xié)調(diào)。
3. 常見的進(jìn)程同步與互斥機(jī)制
常見的進(jìn)程同步與互斥機(jī)制有兩種:
信號量與 PV 操作 管程
① 信號量與 PV 操作
?包交包會(huì)!看完下面這段解釋你絕對能夠明白 PV 操作是啥。
?
1965年,荷蘭學(xué)者 Dijkstra 提出了一種卓有成效的實(shí)現(xiàn)進(jìn)程同步和互斥的方法 — 信號量機(jī)制(Semaphore)。信號量其實(shí)就是一個(gè)變量 ,我們可以用一個(gè)信號量來表示系統(tǒng)中某種資源的數(shù)量,比如:系統(tǒng)中只有一臺打印機(jī),就可以設(shè)置一個(gè)初值為 1 的信號量。
用戶進(jìn)程可以通過使用操作系統(tǒng)提供的一對原語來對信號量進(jìn)行操作,從而很方便的實(shí)現(xiàn)進(jìn)程互斥或同步。這一對原語就是 PV 操作:
1)「P 操作」:將信號量值減 1,表示「申請占用一個(gè)資源」。如果結(jié)果小于 0,表示已經(jīng)沒有可用資源,則執(zhí)行 P 操作的進(jìn)程被阻塞。如果結(jié)果大于等于 0,表示現(xiàn)有的資源足夠你使用,則執(zhí)行 P 操作的進(jìn)程繼續(xù)執(zhí)行。
可以這么理解,當(dāng)信號量的值為 2 的時(shí)候,表示有 2 個(gè)資源可以使用,當(dāng)信號量的值為 -2 的時(shí)候,表示有兩個(gè)進(jìn)程正在等待使用這個(gè)資源。不看這句話真的無法理解 V 操作,看完頓時(shí)如夢初醒。
2)「V 操作」:將信號量值加 1,表示「釋放一個(gè)資源」,即使用完資源后歸還資源。若加完后信號量的值小于等于 0,表示有某些進(jìn)程正在等待該資源,由于我們已經(jīng)釋放出一個(gè)資源了,因此需要喚醒一個(gè)等待使用該資源(就緒態(tài))的進(jìn)程,使之運(yùn)行下去。
我覺得已經(jīng)講的足夠通俗了,不過對于 V 操作大家可能仍然有困惑,下面再來看兩個(gè)關(guān)于 V 操作的問答:
問:「信號量的值 大于 0 表示有臨界資源可供使用,這個(gè)時(shí)候?yàn)槭裁床恍枰獑拘堰M(jìn)程」?
答:所謂喚醒進(jìn)程是從就緒隊(duì)列(阻塞隊(duì)列)中喚醒進(jìn)程,而信號量的值大于 0 表示有臨界資源可供使用,也就是說這個(gè)時(shí)候沒有進(jìn)程被阻塞在這個(gè)資源上,所以不需要喚醒,正常運(yùn)行即可。
問:「信號量的值 等于 0 的時(shí)候表示沒有臨界資源可供使用,為什么還要喚醒進(jìn)程」?
答:V 操作是先執(zhí)行信號量值加 1 的,也就是說,把信號量的值加 1 后才變成了 0,在此之前,信號量的值是 -1,即有一個(gè)進(jìn)程正在等待這個(gè)臨界資源,我們需要喚醒它。
信號量和 PV 操作具體的定義如下:

實(shí)現(xiàn)進(jìn)程互斥
兩步走即可實(shí)現(xiàn)進(jìn)程的互斥:
定義一個(gè)互斥信號量,并初始化為 1 把對于臨界資源的訪問置于 P 操作和 V 操作之間

「P 操作和 V 操作必須成對出現(xiàn)」。缺少 P 操作就不能保證對臨界資源的互斥訪問,缺少 V 操作就會(huì)導(dǎo)致臨界資源永遠(yuǎn)得不到釋放、處于等待態(tài)的進(jìn)程永遠(yuǎn)得不到喚醒。
實(shí)現(xiàn)進(jìn)程同步
回顧一下進(jìn)程同步,就是要各并發(fā)進(jìn)程按要求有序地運(yùn)行。
舉個(gè)例子,以下兩個(gè)進(jìn)程 P1、P2 并發(fā)執(zhí)行,由于存在異步性,因此二者交替推進(jìn)的次序是不確定的。假設(shè) P2 的 “代碼4” 要基于 P1 的 “代碼1” 和 “代碼2” 的運(yùn)行結(jié)果才能執(zhí)行,那么我們就必須保證 “代碼4” 一定是在 “代碼2” 之后才會(huì)執(zhí)行。

如果 P2 的 “代碼4” 要基于 P1 的 “代碼1” 和 “代碼2” 的運(yùn)行結(jié)果才能執(zhí)行,那么我們就必須保證 “代碼4” 一定是在 “代碼2” 之后才會(huì)執(zhí)行。
使用信號量和 PV 操作實(shí)現(xiàn)進(jìn)程的同步也非常方便,三步走:
定義一個(gè)同步信號量,并初始化為當(dāng)前可用資源的數(shù)量 在優(yōu)先級較「高」的操作的「后」面執(zhí)行 V 操作,釋放資源 在優(yōu)先級較「低」的操作的「前」面執(zhí)行 P 操作,申請占用資源

配合下面這張圖直觀理解下:

生產(chǎn)者和消費(fèi)者問題
下面我們利用信號量和 PV 操作來解決經(jīng)典的進(jìn)程同步和互斥問題:生產(chǎn)者和消費(fèi)者問題。
【問題描述】:系統(tǒng)中有一組生產(chǎn)者進(jìn)程和一組消費(fèi)者進(jìn)程,生產(chǎn)者進(jìn)程每次生產(chǎn)一個(gè)產(chǎn)品放入緩沖區(qū),消費(fèi)者進(jìn)程每次從緩沖區(qū)中取出一個(gè)產(chǎn)品并使用。任何時(shí)刻,只能有一個(gè)生產(chǎn)者或消費(fèi)者可以訪問緩沖區(qū)。

由題可知,生產(chǎn)者、消費(fèi)者共享一個(gè)初始為空、大小為 n 的緩沖區(qū),我們從題目中提煉出同步與互斥關(guān)系:
同步關(guān)系 1:只有緩沖區(qū)沒滿時(shí)(優(yōu)先級高),生產(chǎn)者才能把產(chǎn)品放入緩沖區(qū)(優(yōu)先級低),否則必須等待 同步關(guān)系 2:只有緩沖區(qū)不空時(shí)(優(yōu)先級高),消費(fèi)者才能從中取出產(chǎn)品(優(yōu)先級低),否則必須等待 互斥關(guān)系:緩沖區(qū)是臨界資源,各進(jìn)程必須互斥地訪問。
既然這個(gè)題目有兩個(gè)同步關(guān)系和一個(gè)互斥關(guān)系,那么我們就需要兩個(gè)同步信號量和一個(gè)互斥信號量:
empty:同步信號量(對應(yīng)同步關(guān)系 1),表示生產(chǎn)者還能生產(chǎn)多少,即還能放入緩沖區(qū)多少產(chǎn)品,該數(shù)量小于等于 0,則生產(chǎn)者不能進(jìn)行生產(chǎn)。初始化為 n。 full:同步信號量(對應(yīng)同步關(guān)系 2),表示消費(fèi)者還能從緩沖區(qū)取出多少,即當(dāng)前緩沖區(qū)已有產(chǎn)品的數(shù)量,該數(shù)量小于等于 0,則消費(fèi)者不能進(jìn)行讀取。初始化為 0。 mutex:互斥信號量,實(shí)現(xiàn)對緩沖區(qū)的互斥訪問。初始化為 1。
代碼如下,注意各個(gè) PV 操作的配對:

② 管程
管程有一個(gè)重要特性:「在一個(gè)時(shí)刻只能有一個(gè)進(jìn)程使用管程」。進(jìn)程在無法繼續(xù)執(zhí)行的時(shí)候不能一直占用管程,否則其它進(jìn)程將永遠(yuǎn)不能使用管程。也就是說「管程天生支持進(jìn)程互斥」。
其實(shí)使用管程是能夠?qū)崿F(xiàn)信號量的,并且也能用信號量實(shí)現(xiàn)管程。但是管程封裝的比較好,相比起信號量來需要我們編寫的代碼更少,更加易用,這也就是 「Java 采用管程機(jī)制」的原因,synchronized 關(guān)鍵字及 wait()、notify()、notifyAll() 這三個(gè)方法都是管程的組成部分。把管程翻譯為 Java 領(lǐng)域的語言,就是管理類的成員變量和成員方法,讓這個(gè)類是線程安全的。再詳細(xì)的部分就不再深究了,溜了溜了。

?? 點(diǎn)擊下方卡片關(guān)注公眾號「飛天小牛肉」(專注于分享計(jì)算機(jī)基礎(chǔ)、Java 基礎(chǔ)和面試指南的相關(guān)原創(chuàng)技術(shù)好文,幫助讀者快速掌握高頻重點(diǎn)知識,有的放矢),與小牛肉一起成長、共同進(jìn)步
?? 并向大家強(qiáng)烈推薦我維護(hù)的 Gitee 倉庫 「CS-Wiki」(Gitee 推薦項(xiàng)目,目前已 1.0k+ star。致力打造完善的后端知識體系,在技術(shù)的路上少走彎路。相比公眾號,該倉庫擁有更健全的知識體系,歡迎給位小伙伴前來交流學(xué)習(xí),倉庫地址 https://gitee.com/veal98/CS-Wiki。也可直接下方掃碼訪問
原創(chuàng)不易,讀完有收獲不妨點(diǎn)贊|分享|在看支持
