那些煩人的同步和互斥問題

真正的知識是深刻淺出的,碼農翻身」 公共號將苦澀難懂的計算機知識,用形象有趣的生活中實例呈現給咱們,讓咱們更好地理解。程序員

本文源地址:那些煩人的同步和互斥問題編程

一、批處理和脫機打印

打印機程序,準確的說是打印機進程,在這個批處理系統中生活得很是自在,它所在的機器叫作IBM1401,除了打印以外什麼也不幹,天天大部分時間都是歇着。架構

這個系統還有兩臺機器,一臺仍是IBM1401,它專門收集程序員寫出來的穿孔卡片,而後轉成磁帶。  而後,操做員把磁帶輸入到IBM7094這個昂貴又強大的計算機上執行,執行結果也會輸出到磁帶上。 最後磁帶被拿到1401上進行打印, 這叫作脫機打印(不和7094相鏈接)。以下圖:函數

圖1、脫機打印工具

在沒有磁帶來的時候,打印機程序無所事事,這就是脫機打印的好處。更大的好處是,磁帶上須要打印的東西都是順序的,一個接一個打印就能夠了,徹底沒有衝突的問題。這是沒辦法的事情,那時候的計算機,尤爲是IBM 7094太過昂貴,要充分的利用它的每一分每一秒,而後就想出了這樣一個收集程序,而後成批處理的點子。 spa

二、假脫機打印

隨着計算機系統的發展,打印程序的好日子很快就結束了,電腦愈來愈便宜,最後每一個人的桌子上都有一臺電腦了。我的電腦的計算能力更是強大的驚人,打印程序也被集成進了我的電腦裏,和其餘各類各樣的程序生活在一塊兒。打印的需求仍然很強烈,像Word、WPS、Excel 、 IE、Chrome...這些程序時不時都要打印,這時候衝突就會產生。 由於只有一個打印機,到底先打印誰的文檔就是個大問題。最後操做系統老大想了個辦法,專門開闢了一塊空間,誰要想打印的話就按照先來後到的次序排隊放在那,原來的打印進程變成了一個打印守護進程,會週期性的檢查是否有文件打印,若是有則取出隊伍排頭的,打印出來,而後刪除隊列中文件。打印進程以爲這和原來的脫機打印很像,只不過用一個隊列替換了原來的磁帶,因此就叫作假脫機打印操作系統

圖2、假脫機打印線程

三、衝突

可是這個隊列可不是原來的磁帶了,它徹底是個動態變化的東西,試運行還不到20秒,衝突就出現了。 code

WPS氣沖沖的來着打印機進程:「打印機,你怎麼搞的?個人放假通知.wps 爲何沒有打印?」 blog

打印機:「我沒看到什麼放假通知.wps啊!」

WPS:「我明明放在了編號爲3的槽裏,怎麼可能沒有了?」

在操做系統老大的協助下,你們查了半天,才知道是Word引發的:

當時Word 插了一腳,也進來打印,讀到了in = 3,就是說隊列中編號爲3的槽是空着的,他把3這個值放到了本身的局部變量free_slot中,這時候發生了一次時鐘中斷,操做系統老大認爲Word已經運行了足夠長的時間,決定切換到WPS進程。 

WPS也讀到了in = 3,把3 也存到本身的局部變量free_slot中,如今Word和WPS都認爲下一個空的槽是3!

WPS接着幹活,他把文件放到了第3號槽裏,而且把in 改成4,而後離開了。接下來又輪到Word運行了,它發現free_slot 爲3,就把文件也放到了第3號槽裏,把free_slot 加1,獲得4,存入in 中。可憐的WPS,他的文件被覆蓋掉了。

可是打印機程序啥也察覺不出來,照樣打印不誤。  

圖3、衝突

四、臨界區

很明顯,Word和WPS 這兩個進程甚至多個進程在讀寫in這個共享變量的時候,最後的結果嚴重依賴於進程運行的精確次序,此次是WPS的文件被覆蓋掉了,下次可能就是Word了。 
這種對共享變量,共享內存,共享資源進行訪問的程序片斷叫作臨界區。代碼在進入臨界區以前必定要作好同步或者互斥的操做。
WPS說:「老大,當時你切換Word的時候是否是發生了一次時鐘中斷 ?」
操做系統:「是啊,有了時鐘中斷我才能計算時間,而後作進程切換啊!」
「那在訪問這個in共享變量的時候,咱們本身能不能把這個中斷給屏蔽?這樣就不會有進程切換,確定沒問題了。」 Word問道。
「你想的美,時鐘中斷是最基本的東西,我把這個權限給了大家應用程序,到時候那個傢伙屏蔽之後忘記開中斷,咱們整個系統就要完蛋了!」 操做系統狠狠的瞪了Word 一眼,Word趕忙噤聲。 
「不過我據說有些機器提供了一個特別的指令,這個指令能檢查而且設置內存的值,而不會被打斷,叫作TestAndSet,若是用C語言描述的話,相似這樣:」

bool TestAndSet(bool *lock){
  bool rv = *lock;
  *lock = true;
  return tv;      
}

「須要注意的是,這個函數中的三條指令是「原子」執行的,也就是說不會被打斷。大家要是想用的話能夠這樣用:」

bool lock = false;
while(TestAndSet(&lock)){
  ;//什麼也不作  
}

臨界區;
lock = false;
剩餘區;

WPS說:「看起來有點複雜,讓我想一想啊,我和Word 的臨界區代碼,就是‘訪問in變量,放入待打印文件,而後把in 加1’ 。那在進入臨界區以前,咱們倆都會調用TestAndSet。若是是我先調用,lock會被置爲true,函數就會返回false。我就跳出了循環,能夠進行後續臨界區操做了,而Word 在調用 TestAndSet的時候,函數一直返回true,他只好不停的在這裏循環了。」
「是啊!」,Word 接着說,「我會不停的循環,直到WPS 離開臨界區,而後把lock置爲false。 」
「這個方法看起來很簡單啊,只要一個變量加上一個函數就能讓我和Word 進行互斥操做。」
操做系統說: 「是的,實現了大家兩個的互斥,可是並非全部的機器都會提供這樣的指令,因此也不通用。」

五、生產者-消費者

打印機進程說:「大家討論了半天,只是解決了兩個進程往隊列裏放文件的衝突問題,如今也得考慮考慮我了。」
「有你啥事?」,Word和WPS 都不覺得然。
「大家想一想,那個打印隊列對5個‘槽’,要是滿了就無法往裏邊放了,大家都得等;要是空了,我就得等大家往裏邊放東西,因此我們之間是否是也得同步?」
「這就是所謂的生產者和消費者問題,也是個老大難問題了」,老大總結道。 
「那用剛纔那個鎖好像不行啊,它能搞定互斥,可是作多個進程的同步就有點力不從心了。」
操做系統老大說:「據說荷蘭有個叫Dijkstra的,發明了一個信號量(semaphore)的東西,能解決這個問題。」

 

圖4、科學家Dijkstra.

「信號量是什麼鬼?信號燈嗎? 」
"所謂信號量,說白了其實就是一個整數,基於這個整數有兩個操做:wait 和 signa。」

int s;
wait(s){
  while(s <= 0){
        ;//什麼也不作
    }  
    s--;
}        

signal(s){
  s++;  
}

「這....這....這是啥玩意兒,這麼簡單,能解決啥問題?再說了你看看這s++、s--和咱們隊列中的in、out不是同樣嗎?在多進程切換下自身正確性都難保,還能解決別人的問題?」 ,WPS吃驚的問。
「WPS 問的好啊,說明他思考了,實際上這個東西必須得我出馬來實現」,操做系統老大說,「 我會在內核實現wait 和signal,讓大家調用,好比我在作s++、s-- 時,我能夠屏蔽中斷。」
 Word說:「這個簡單的小東西有點意思,好比咱們倆能夠用它作互斥:」

int lock = 1;
wait(lock);
臨界區;
signal(lock);
剩餘區;

打印進程說:「既然信號量是個整數,也許能夠解決咱們消費者-生產者直接的同步問題。」

int lock = 1;
int empty = 5;
int full = 0;

生產者:
while(true){
  //若是empty的值小於等於0,生產者只好等待
  wait(empty);
  //加鎖(由於要操做隊列,和其它生產者互斥)
  wait(lock);
  把新產生的文件加入隊列;
  //釋放鎖
  signal(lock);
  //通知消費者隊列中已經產生了新的文件
  signal(full);     
}

消費者:
while(true){
  wait(full);
  wait(lock);
  把隊列頭的文件打印,刪除;
  signal(lock);
  signal(empty);
}

Word說:「個人天,真是複雜啊,容我想一想,我和WPS都是生產者。假設咱們倆都開始執行生產者代碼,先去wait(empty),發現沒有問題,由於empty的初始值爲5。接下來都去執行wait(lock),這時候就看誰先搶到了。若是我先搶到,我就能夠往隊列里加文件,而後釋放鎖,WPS就能夠接着放文件了。最後我還要把full這個值加一,目的是打印機進程可能在等待。恩,看起來不錯!」
操做系統老大說:「是啊!在多進程下,因爲進程的執行隨時都有可能被打斷,還要保證正確性,不能出一點閃失。這對程序員的挑戰很大,出現了疏漏,很難定位。」
打印進程說:「老大,我注意到wait函數中,若是s 的值 爲0或小於0 ,那個while 循環會一直執行,CPU豈不是一直在忙等?」
「確實是這樣,咱們改進下,讓忙等的進程進入休眠吧,很明顯,這件事還得我作啊」, 操做系統說道。

//把整數型信號量封裝成一個結構體
typedef struct{
  int value;
  struct process *list;//process進程就緒隊列
}semaphore;

wait(semaphore *s){
  s->value--;
  if(s->value < 0){
    把當前進程加到s->list中;
    block();//把進程休眠,放棄cpu
  }
}

signal(semaphore *s){
  s->value++;
  if(s->value <= 0){
    從s->list中取出一個進程p
    wakup(p);//喚醒進程p
  }
}

WPS說:「唉,真是好複雜!不過我想起一個問題,這些wait、signal 能用到咱們內部的線程的同步上嗎?」
「固然能夠,概念上是一致的,都是訪問共享資源的程序,須要作同步和互斥操做,可能表現形式不一樣。」
「難道那些程序員們真的要使用這些wait、signal 編程嗎?多容易出錯啊!」
「通常來講,程序員們所使用的工具和平臺會作抽象和封裝,例如在Java JDK中,已經對線程的同步作了封裝了,對於生產者-消費者問題,能夠直接使用BlockingQueue。很是簡單,徹底不用你去考慮這些wait、signal、full、empty。」

//創建一個隊列,其中隊列中已經對wait和signal操做作了封裝
BlockingQueue queues = new LinkedBlockingQueue(10);

生產者:
//若是隊列滿,線程自動阻塞,直到有空閒位置
queues.put(xxx);

消費者:
//若是隊列空,線程自動阻塞,直到有數據到來
queues.take();

WPS說:「果真是抽象大法好,這多簡單啊。」

操做系統說:「是啊!不管是什麼東西,抽象之後用起來好多了。可是仍是要了解底層,這樣出現了相似於BlockingQueue這樣的新概念, 你能迅速搞明白。」

(完)

「碼農翻身」 公共號:由工做15年的前IBM架構師建立,分享編程和職場的經驗教訓。

長按二維碼, 關注碼農翻身

相關文章
相關標籤/搜索