第八章-----執行緒同步

飄過的小熊發表於2016-09-12

標籤(空格分隔): 作業系統之哲學原理


為什麼要同步


最直觀的例子是

執行緒A:x=1;
執行緒B:x=2;

這兩個執行緒執行結束後會有以下幾種情況:

  • x=1;
  • x=2;
    甚至可以是3.

由於x是共享變數,且執行緒之間的相對執行順序是不確定的,因此執行緒A可以線上程B的前後執行。至於3的情況,根據指令集結構在執行賦值語句時的動作而定。

兩個執行緒在執行的時候會進行穿插:

執行緒A
i=0;
while(i<10){
i++
}
printf”A finished”;

還有

執行緒B
i=0;
while(i>10){
i–;
}
printf”B finished”;

不知道哪一個執行緒會先結束

這裡也是折射一個看上去比較矛盾的問題:我們使用多執行緒併發,但是針對其中某一個程式來說我們無法確定這個程式在什麼時間可以結束,因為程式併發交叉執行。於是就想到同步。

執行緒同步的目的


執行緒同步的目的就是不管執行緒之間的執行如何穿插,其執行結果都是正確的。或者說,要保證多執行緒執行下結果的正確性。還有一個目的就是關於執行效率的問題。

同步就是讓所有執行緒按照一定的規則執行,使得其正確性和效率都有跡可尋,執行緒同步的手段就是對執行緒之間的穿插進行控制

鎖的進化:金魚生存


講的是兩個人一起養金魚給金魚餵食,金魚不能多吃,多了就撐死;金魚不能少食,少了就餓死。因此兩個人餵食很有講究。他們基本不會天天都在一起,因此什麼時候適合給金魚餵食就很重要。餵食之前先要判斷對方是否已經給金魚餵食,要是沒有喂,就得餵食;要是已經餵了,就不再繼續餵食。
兩個或者多個執行緒爭相執行同一段程式碼或訪問同一資源的現象稱為競爭。這個可能造成競爭的共享程式碼段或資源稱為臨界區
兩個執行緒不可能在同一時刻執行,但是有可能在同一時刻兩個執行緒在同一段程式碼上

變形蟲階段


要防止魚撐死就要防止競爭,要避免競爭就要防止兩個或多個執行緒同時進入臨界區。一次需要協調手段。
協調的目的就是在任何時候都只能有一個人在臨界區,這就是傳說中的互斥(mutual exclusion),互斥就是說一次只有一個人使用共享資源,還不能違反互斥模型
正確的互斥:

  • 不能有兩個程式同時在臨界區
  • 程式能在任何數量和速度的CPU上執行
  • 在互斥區外不能阻止另一個程式的執行
  • 程式不能無限地等待進入臨界區

任何一個條件不滿足,設計的互斥就不正確
於是改進餵魚的手段,決定每個人要是餵了魚就留一個紙條。

但是這樣的方式並沒有達到互斥的目的,因為沒有防止兩個人同時進入臨界區。但是降低了魚撐死的概率。這個案例的臨界區就是站在魚缸前面準備投魚食 撐死可能是:A在魚缸前發現沒有B的紙條,好,魚很餓。丟幾袋魚食。還沒來得及留下紙條。好,執行緒切換,B來到了魚缸前,發現沒有紙條,好傢伙魚還沒餵食呢,一股腦扔幾袋魚食。一分鐘後,不幸的事情發生了,魚撐死了。執行緒切換可以想象成電影裡面的場景切換。

魚階段


上面的餵魚情景中,發現不解決問題的原因是我們先檢查紙條,再餵魚,再留紙條。餵魚和留紙條之間出現了一個空檔。我們的解決辦法就是先留紙條再檢查有沒有對方的紙條,喂完後把紙條拿開。不過需要區分紙條是誰的。
總有一個人的留紙條指令在另一個人的檢查紙條指令之前執行,從而將防止兩個人同時進入臨界區

但是穿插執行的話又有新問題:

A來到魚缸前,先留下紙條。可是在這個時候發生了執行緒切換,B來到魚缸前先留下紙條,檢查有沒有對方的紙條,發現對方已經留下了紙條,於是離開。執行緒切換,A開始檢查對方紙條,發現對方已經留下了紙條。於是離開。於是,。,。,。魚就悲劇了。沒有餵食。

比起撐死來說,餓死也是一種改善
如果是撐死,程式的執行結果很可能出錯:幾個執行緒同時獲得同一個資源,出現的不一致性及結果不確定性幾乎是難以避免的。但是如果是餓死,大家都拿不到資源,執行緒處於飢餓狀態,至多是停止推進,而這不一定產生錯誤結果,或許只是推遲結果的出現。

猴階段


上面的魚被餓死是因為沒有人進入臨界區。於是想出了另一個同步機制:迴圈等待;
辦法就是:讓某一個人等著,直到確認有人餵了魚才離去,不能見到對方的紙條就開溜。

下面的場景:
1.A來到魚缸前,先留下紙條。檢查沒有對方的紙條,餵魚。喂完魚後刪除紙條。B來到魚缸前,先留下紙條。檢查沒有A的紙條,開始餵魚。喂完後刪除紙條。成功。
2.A來到魚缸前,先留下紙條。檢查有沒有對方紙條,好,程式切換。切換至B來到魚缸前,先留下紙條,再檢查有沒有A留下的紙條,發現有。於是,。,。,。,。。,。不能離開,等待,。,。,。等到執行緒切換回A執行緒,A喂完魚後刪除紙條。檢查魚有沒有被喂。有就離去。

幾個場景邏輯不是很縝密,但是意思肯定是到位了,理解了意思不要糾結於我們模擬出來的細節


上面的幾種同步機制雖然正確

  • 程式不對稱就不美觀
  • 程式不對稱就編寫困難
  • 迴圈等待是對CPU的極大浪費,浪費就是犯罪。

設計一種同步措施,使得在任何時候只能有一個人進入放置魚缸的房間,這樣檢查紙條和留紙條都可以變成房間上鎖的一步操作,在作業系統中,這種可以保證互斥的同步機制稱為鎖

鎖的基本操作


  • 閉鎖操作
    • 步驟1:等待鎖達到開啟裝態
    • 步驟2:獲得鎖並鎖上
  • 開鎖:開啟鎖

顯然閉鎖的兩個操作應該是原子操作,不能分開。

正常鎖的基本特性:

  • 鎖的初始狀態是開啟狀態
  • 進臨界區前必須獲得鎖
  • 出臨界區時必須開啟鎖
  • 如果別人持有鎖則必須等待

    迴圈等待很痛苦呀,部不僅造成了浪費,還降低了系統效率。但是不能消除繁忙等待,可以減少等待的時間。

睡覺與叫醒:生產者與消費者問題


什麼是睡覺與叫醒?

如果對方持有鎖,你就不需要等待鎖變為開啟狀態,而是去睡覺,鎖開啟後對方再來把你叫醒。

sleepwakeup就是作業系統裡的睡覺和叫醒原語。一個程式呼叫sleep後進入休眠狀態,將釋放其所佔的CPU.一個執行wakeup的程式將傳送一個訊號給指定的接收程式。

訊號量(semphore)


訊號量的功能:

  • 同步原語
  • 通訊原語

訊號量作為同步原語和鎖簡單來說就是一個計數器,其取值就是當前累積的訊號數量。
訊號量支援的操作:

  • down減法操作
    • 判斷訊號量的取值是否大於等於1
    • 如果是,將訊號量的值減1,繼續往下執行
    • 否則在該訊號量上等待(執行緒被掛起)
  • up加法操作
    • 將訊號量的值增加1(此操作將叫醒一個在該訊號量上面等待的執行緒)
    • 執行緒繼續往下執行

看上去很多的操作,但是這些操作都是原子操作,是不能分開的。

將訊號量的取值限制為0和1兩種情況則獲得的就是一把鎖。也稱為二元訊號量。

鎖、睡覺與叫醒、訊號量

解決了同步問題,但帶來了迴圈等待,為了消除迴圈等待,我們發明了睡覺與叫醒,但是睡覺與叫醒又帶來了死鎖,於是發明了訊號量。使用訊號量的時候,操作順序很是重要,稍有不慎就會發生死鎖,於是我們發明了管程

管程

訊息傳遞

柵欄

相關文章