1. 什麼是程式同步
在多道批處理系統中,多個程式是可以併發執行的,但由於系統的資源有限,程式的執行不是一貫到底的, 而是走走停停,以不可預知的速度向前推進,這就是程式的非同步性。
那麼,程式的非同步性會帶來什麼問題呢?舉個例子,如果有 A、B 兩個程式分別負責讀和寫資料的操作,這兩個執行緒是相互合作、相互依賴的。那麼寫資料應該發生在讀資料之前。而實際上,由於非同步性的存在,可能會發生先讀後寫的情況,而此時由於緩衝區還沒有被寫入資料,讀程式 A 沒有資料可讀,因此讀程式 A 被阻塞。
程式同步(synchronization)就是用來解決這個問題的。從上面的例子我們能看出,一個程式的執行可能影響到另一個程式的執行,所謂程式同步就是指協調這些完成某個共同任務的併發執行緒,在某些位置上指定執行緒的先後執行次序、傳遞訊號或訊息。
再舉個生活中的程式同步的例子,你想要喝熱水,於是你打了一壺水開始燒,在這壺水燒開之前,你只能一直等著,水燒開之後水壺自然會發生響聲提醒你來喝水,於是你就可以喝水了。就是說水燒開這個事情必須發生在你喝水之前。
注意不要把程式同步和程式排程搞混了:
-
程式排程是為了最大程度的利用 CPU 資源,選用合適的演算法排程就緒佇列中的程式。
-
程式同步是為了協調一些程式以完成某個任務,比如讀和寫,你肯定先寫後讀,不能先讀後寫吧,這就是程式同步做的事情了,指定這些程式的先後執行次序使得某個任務能夠順利完成。
2. 什麼是程式互斥
同樣的,也是因為程式的併發性,併發執行的執行緒不可避免地需要共享一些系統資源,比如記憶體、印表機、攝像頭等。舉個例子:我們去學校列印店列印論文,你按下了 WPS 的 “列印” 選項,於是印表機開始工作。 你的論文列印到一半時,另一位同學按下了 Word 的 “列印” 按鈕,開始列印他自己的論文。想象一下如果兩個程式可以隨意的、併發的共享印表機資源,會發生什麼情況?
顯然,兩個程式併發執行,導致印表機裝置交替的收到 WPS 和 Word 兩個程式發來的列印請求,結果兩篇論文的內容混雜在一起了。
程式互斥(mutual exclusion)就是用來解決這個問題的。當某個程式 A 在訪問印表機時,如果另一個程式 B 也想要訪問印表機,它就必須等待,直到 A 程式訪問結束並釋放印表機資源後,B 程式才能去訪問。
實際上,像上述的印表機這種在一個時間段內只允許一個程式使用的資源(這也就是互斥的意思),我們將其稱為臨界資源,對臨界資源進行訪問的那段程式碼稱為臨界區。
通俗的對比一下程式互斥和程式同步:
-
程式同步:程式 A 應在程式 B 之前執行
-
程式互斥:程式 A 和程式 B 不能在同一時刻執行
從上不難看出,程式互斥是一種特殊的程式同步,即逐次使用臨界資源,也是對程式使用資源的先後執行次序的一種協調。
3. 常見的程式同步與互斥機制
常見的程式同步與互斥機制有兩種:
-
訊號量與 PV 操作
-
管程
① 訊號量與 PV 操作
包交包會!看完下面這段解釋你絕對能夠明白 PV 操作是啥。
1965年,荷蘭學者 Dijkstra 提出了一種卓有成效的實現程式同步和互斥的方法 — 訊號量機制(Semaphore)。訊號量其實就是一個變數 ,我們可以用一個訊號量來表示系統中某種資源的數量,比如:系統中只有一臺印表機,就可以設定一個初值為 1 的訊號量。
使用者程式可以通過使用作業系統提供的一對原語來對訊號量進行操作,從而很方便的實現程式互斥或同步。這一對原語就是 PV 操作:
1)P 操作:將訊號量值減 1,表示申請佔用一個資源。如果結果小於 0,表示已經沒有可用資源,則執行 P 操作的程式被阻塞。如果結果大於等於 0,表示現有的資源足夠你使用,則執行 P 操作的程式繼續執行。
可以這麼理解,當訊號量的值為 2 的時候,表示有 2 個資源可以使用,當訊號量的值為 -2 的時候,表示有兩個程式正在等待使用這個資源。不看這句話真的無法理解 V 操作,看完頓時如夢初醒。
2)V 操作:將訊號量值加 1,表示釋放一個資源,即使用完資源後歸還資源。若加完後訊號量的值小於等於 0,表示有某些程式正在等待該資源,由於我們已經釋放出一個資源了,因此需要喚醒一個等待使用該資源(就緒態)的程式,使之執行下去。
我覺得已經講的足夠通俗了,不過對於 V 操作大家可能仍然有困惑,下面再來看兩個關於 V 操作的問答:
問:訊號量的值 大於 0 表示有臨界資源可供使用,這個時候為什麼不需要喚醒程式?
答:所謂喚醒程式是從就緒佇列(阻塞佇列)中喚醒程式,而訊號量的值大於 0 表示有臨界資源可供使用,也就是說這個時候沒有程式被阻塞在這個資源上,所以不需要喚醒,正常執行即可。
問:訊號量的值 等於 0 的時候表示沒有臨界資源可供使用,為什麼還要喚醒程式?
答:V 操作是先執行訊號量值加 1 的,也就是說,把訊號量的值加 1 後才變成了 0,在此之前,訊號量的值是 -1,即有一個程式正在等待這個臨界資源,我們需要喚醒它。
訊號量和 PV 操作具體的定義如下:
實現程式互斥
兩步走即可實現程式的互斥:
-
定義一個互斥訊號量,並初始化為 1
-
把對於臨界資源的訪問置於 P 操作和 V 操作之間
P 操作和 V 操作必須成對出現。缺少 P 操作就不能保證對臨界資源的互斥訪問,缺少 V 操作就會導致臨界資源永遠得不到釋放、處於等待態的程式永遠得不到喚醒。
實現程式同步
回顧一下程式同步,就是要各併發程式按要求有序地執行。
舉個例子,以下兩個程式 P1、P2 併發執行,由於存在非同步性,因此二者交替推進的次序是不確定的。假設 P2 的 “程式碼4” 要基於 P1 的 “程式碼1” 和 “程式碼2” 的執行結果才能執行,那麼我們就必須保證 “程式碼4” 一定是在 “程式碼2” 之後才會執行。
如果 P2 的 “程式碼4” 要基於 P1 的 “程式碼1” 和 “程式碼2” 的執行結果才能執行,那麼我們就必須保證 “程式碼4” 一定是在 “程式碼2” 之後才會執行。
使用訊號量和 PV 操作實現程式的同步也非常方便,三步走:
-
定義一個同步訊號量,並初始化為當前可用資源的數量
-
在優先順序較高的操作的後面執行 V 操作,釋放資源
-
在優先順序較低的操作的前面執行 P 操作,申請佔用資源
配合下面這張圖直觀理解下:
生產者和消費者問題
下面我們利用訊號量和 PV 操作來解決經典的程式同步和互斥問題:生產者和消費者問題。
【問題描述】:系統中有一組生產者程式和一組消費者程式,生產者程式每次生產一個產品放入緩衝區,消費者程式每次從緩衝區中取出一個產品並使用。任何時刻,只能有一個生產者或消費者可以訪問緩衝區。
由題可知,生產者、消費者共享一個初始為空、大小為 n 的緩衝區,我們從題目中提煉出同步與互斥關係:
-
同步關係 1:只有緩衝區沒滿時(優先順序高),生產者才能把產品放入緩衝區(優先順序低),否則必須等待
-
同步關係 2:只有緩衝區不空時(優先順序高),消費者才能從中取出產品(優先順序低),否則必須等待
-
互斥關係:緩衝區是臨界資源,各程式必須互斥地訪問。
既然這個題目有兩個同步關係和一個互斥關係,那麼我們就需要兩個同步訊號量和一個互斥訊號量:
-
empty:同步訊號量(對應同步關係 1),表示生產者還能生產多少,即還能放入緩衝區多少產品,該數量小於等於 0,則生產者不能進行生產。 初始化為 n。
-
full:同步訊號量(對應同步關係 2),表示消費者還能從緩衝區取出多少,即當前緩衝區已有產品的數量,該數量小於等於 0,則消費者不能進行讀取。初始化為 0。
-
mutex:互斥訊號量,實現對緩衝區的互斥訪問。初始化為 1。
程式碼如下,注意各個 PV 操作的配對:
② 管程
管程有一個重要特性:在一個時刻只能有一個程式使用管程。程式在無法繼續執行的時候不能一直佔用管程,否則其它程式將永遠不能使用管程。也就是說管程天生支援程式互斥。
其實使用管程是能夠實現訊號量的,並且也能用訊號量實現管程。但是管程封裝的比較好,相比起訊號量來需要我們編寫的程式碼更少,更加易用,這也就是 Java 採用管程機制的原因,synchronized
關鍵字及 wait()
、notify()
、notifyAll()
? 關注公眾號 | 飛天小牛肉,即時獲取更新
-
-
並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.4k+ star)