為了能夠有效的控制多個進程之間的溝通過程,保證溝通過程的有序和和諧,OS必須提供一定的同步機制保證進程之間不會自說自話而是有效的協同工作。比如在共享內存的通信方式中,兩個或者多個進程都要對共享的內存進行數據寫入,那么怎么才能保證一個進程在寫入的過程中不被其它的進程打斷,保證數據的完整性呢?又怎么保證讀取進程在讀取數據的過程中數據不會變動,保證讀取出的數據是完整有效的呢?
常用的同步方式有: 互斥鎖、條件變量、讀寫鎖、記錄鎖(文件鎖)和信號燈。。
1、互斥鎖
顧名思義,鎖是用來鎖住某種東西的,鎖住之后只有有鑰匙的人才能對鎖住的東西擁有控制權(把鎖砸了,把東西偷走的小偷不在我們的討論范圍了)。所謂互斥,從字面上理解就是互相排斥。因此互斥鎖從字面上理解就是一點進程擁有了這個鎖,它將排斥其它所有的進程訪問被鎖住的東西,其它的進程如果需要鎖就只能等待,等待擁有鎖的進程把鎖打開后才能繼續運行。
在實現中,鎖並不是與某個具體的變量進行關聯,它本身是一個獨立的對象。進(線)程在有需要的時候獲得此對象,用完不需要時就釋放掉。
互斥鎖的主要特點是互斥鎖的釋放必須由上鎖的進(線)程釋放,如果擁有鎖的進(線)程不釋放,那么其它的進(線)程永遠也沒有機會獲得所需要的互斥鎖。互斥鎖主要用於線程之間的同步。
2、條件變量
上文中提到,對於互斥鎖而言,如果擁有鎖的進(線)程不釋放鎖,其它進(線)程永遠沒機會獲得鎖,也就永遠沒有機會繼續執行后續的邏輯。在實際環境下,一個線程A需要改變一個共享變量X的值,為了保證在修改的過程中X不會被其它的線程修改,線程A必須首先獲得對X的鎖。現在假如A已經獲得鎖了,由於業務邏輯的需要,只有當X的值小於0時,線程A才能執行后續的邏輯,於是線程A必須把互斥鎖釋放掉,然后繼續“忙等”。如下面的偽代碼所示:
// get x lock while(x <= 0){ // unlock x ; // wait some time // get x lock } // unlock x
這種方式是比較消耗系統的資源的,因為進程必須不停的主動獲得鎖、檢查X條件、釋放鎖、再獲得鎖、再檢查、再釋放,一直到滿足運行的條件的時候才可以。因此我們需要另外一種不同的同步方式,當線程X發現被鎖定的變量不滿足條件時會自動的釋放鎖並把自身置於等待狀態,讓出CPU的控制權給其它線程。其它線程此時就有機會去修改X的值,當修改完成后再通知那些由於條件不滿足而陷入等待狀態的線程。這是一種通知模型的同步方式,大大的節省了CPU的計算資源,減少了線程之間的競爭,而且提高了線程之間的系統工作的效率。這種同步方式就是條件變量。
坦率的說,從字面意思上來將,“條件變量”這四個字是不太容易理解的。我們可以把“條件變量”看做是一個對象,一個鈴鐺,一個會響的鈴鐺。當一個線程在獲得互斥鎖之后,由於被鎖定的變量不滿足繼續運行的條件時,該線程就釋放互斥鎖並把自己掛到這個“鈴鐺”上。其它的線程在修改完變量后,它就搖搖“鈴鐺”,告訴那些掛着的線程:“你們等待的東西已經變化了,都醒醒看看現在的它是否滿足你們的要求。”於是那些掛着的線程就知道自己醒來看自己是否能繼續跑下去了。
3、讀寫鎖
互斥鎖是排他性鎖,條件變量出現后和互斥鎖配合工作能夠有效的節省系統資源並提高線程之間的協同工作效率。互斥鎖的目的是為了獨占,條件變量的目的是為了等待和通知。但是現實世界是很復雜di,我們要解決的問題也是多種多樣di.從功能上來說,互斥鎖和條件變量能夠解決基本上所有的問題,但是性能上就不一定完全滿足了。人的無休止的欲望促使人發明出針對性更強、性能更好的同步機制來。讀寫鎖就是這么一個玩意兒。
考慮一個文件有多個進程要讀取其中的內容,但只有1個進程有寫的需求。我們知道讀文件的內容不會改變文件的內容,這樣即使多個進程同時讀相同的文件也沒什么問題,大家都能和諧共存。當寫進程需要寫數據時,為了保證數據的一致性,所有讀的進程就都不能讀數據了,否則很可能出現讀出去的數據一半是舊的,一半是新的狀況,邏輯就亂掉了。
為了防止讀數據的時候被寫入新的數據,讀進程必須對文件加上鎖。現在假如我們有2個進程都同時讀,如果我們使用上面的互斥鎖和條件變量,當其中一個進程在讀取數據的時候,另一個進程只能等待,因為它得不到鎖。從性能上考慮,等待進程所花費的時間是完全的浪費,因為這個進程完全可以讀文件內容而不會影響第一個,但是這個進程沒有鎖,所以它什么也做不了,只能等,等到花兒都謝了。
所以呢,我們需要一種其它類型的同步方式來滿足上面的需求,這就是讀寫鎖。
讀寫鎖的出現能夠有效的解決多進程並行讀的問題。每一個需要讀取的進程都申請讀鎖,這樣大家互不干擾。當有進程需要寫如數據時,首先申請寫鎖。如果在申請時發現有讀(或者寫)鎖存在,則該寫進程必須等待,一直等到所有的讀(寫)鎖完全釋放為止。讀進程在讀取之前首先申請讀鎖,如果所讀數據被寫鎖鎖定,則該讀進程也必須等待讀鎖被釋放位置。
很自然的,多個讀鎖是可以共存的,但寫鎖是完全互相排斥的。
4、記錄鎖(文件鎖)
為了增加並行性,我們可以在讀寫鎖的基礎上進一步細分被鎖對象的粒度。比如一個文件中,讀進程可能需要讀取該文件的前1k個字節,寫進程需要寫該文件的最后1k個字節。我們可以對前1k個字節上讀鎖,對最后1k個自己上寫鎖,這樣兩個進程就可並發工作了。記錄鎖中的所謂“記錄”其實是“內容”的概念。使用讀寫鎖可以鎖定一部分,而不是整個文件。
文件鎖可以認為是記錄鎖的一個特例,當使用記錄鎖鎖定文件的所有內容時,此時的記錄鎖就可以稱為文件鎖了。
5、信號燈
信號燈可以說是條件變量的升級版。條件變量相當於鈴鐺,鈴鐺響后每個掛起的進程還需要自己獲得互斥鎖並判斷所需條件是否滿足,信號燈把這兩步操作糅合到一起。
在Posix.1基本原理一文聲稱,有了互斥鎖和條件變量還提供信號燈的原因是:“本標准提供信號燈的而主要目的是提供一種進程間同步的方式;這些進程可能共享也可能不共享內存區。互斥鎖和條件變量是作為線程間的同步機制說明的;這些線程總是共享(某個)內存區。這兩者都是已廣泛使用了多年的同步方式。每組原語都特別適合於特定的問題”。盡管信號燈的意圖在於進程間同步,互斥鎖和條件變量的意圖在於線程間同步,但是信號燈也可用於線程間,互斥鎖和條件變量也可用於進程見。應當根據實際的情況進行決定。
信號燈最有用的場景是用以指明可用資源的數量。比如含有10個元素的數組,我們可以創建一個信號燈,初始值為0.每當有進程需要讀數組中元素時(假設每次僅能讀取1個元素),就申請使用該信號燈(信號燈的值減1),當有進程需要寫元素時,就申請掛出該信號等(信號燈值加1)。這樣信號燈起到了可用資源數量的作用。如果我們限定信號燈的值只能取0和1,就和互斥鎖的含義很相同了。