死鎖及其預防策略


什么是死鎖?

  • 如果一個進程集合中的每個進程都在等待只能只能有該集合中的其他一個進程才能引發的事件, 這種情況就是死鎖。

  • 簡單舉例

    • 資源 A 與 資源 B 都是不可剝奪資源

    • 進程 C 已經申請到資源A, 進程D已經申請到了資源B

    • 進程 C 此時申請資源B, 而進程D恰好申請了資源A

    • 由於資源已被占用, 進程A和進程B都無法執行下一步操作, 就造成了死鎖。

      image-20191109150926746

產生死鎖的四個必要條件

  • 互斥條件 (Mutual exclusive)
    • 資源不能被共享, 只能由一個進程使用。
  • 請求與保持條件 (Hold and Wait)
    • 已經得到資源的進程可以再次申請新的資源。
  • 非剝奪條件 (No pre-emption)
    • 已經分配的資源不能從相應的進程強制地剝奪。
  • 循環等待條件 (Circular wait)
    • 系統中若干進程組成環路, 該環路中每個進程都在等待相鄰進程整占用的資源。

死鎖處理策略

  • 預防死鎖

    • 設置某些限制條件, 破壞產生死鎖的四個必要條件中的一個或幾個, 以預防發生死鎖。
  • 避免死鎖

    • 在資源動態分配中, 用某種方法防止系統進入不安全狀態, 從而避免死鎖。
  • 死鎖 檢測 和 解除

    • 無需采取任何限制性措施,允許進程在運行過程中發生死鎖。通過系統檢測機構及時地檢測死鎖的發生,然后采取某種措施解除死鎖。
    資源分配策略 各種可能模式 主要優點 主要缺點
    死鎖預防 保守, 寧可資源限制 一次請求所有的資源, 資源剝奪, 資源按序分配 適用於做突發式處理的進程, 不必進行剝奪。 效率低, 進程初始化時間延長; 剝奪次數過多; 不便靈活申請新資源。
    死鎖避免 “預防” 和 “檢測” 的折中 尋找可能的安全允許順序 不必進行剝奪 必須知道將來的資源需求; 進程不能被長時間阻塞。
    死鎖檢測 寬松, 只要允許就分配資源 定期檢查死鎖是否已經發生 不延長進程初始化時間, 允許對死鎖進行現場處理。 通過剝奪解除死鎖造成的損失。
  • 銀行家算法:

    • 把操作系統看做是銀行家,操作系統管理的資源相當於銀行家管理的資金,進程向操作系統請求 分配資源相當於用戶向銀行家貸款。
    • 操作系統按照銀行家制定的規則為進程分配資源,當進程首次申請資源時,要測試該進程對資源的最大需求量,如果系統現存的資源可以滿足它的最大需求量則按當前的申請量分配資源, 否則就推遲分配。
    • 當進程在執行中繼續申請資源時,先測試該進程已占用的資源數和本次申請的資源數之和是否超過了該進程對資源的最大的需求量。
    • 若超過則拒絕分配資源,過沒有超過則再測試系統現存的資源能否滿足該進程尚需的最大的資源量,若能滿足則按當前的申請分配資源,否則也要推遲。
  • 解除死鎖的方法

    • 資源剝奪法:掛起某些死進程並搶奪它的資源,以便讓其他進程繼續推進
    • 撤銷進程法:強制撤銷部分,甚至全部死鎖進程並剝奪這些進程的資源
    • 進程回退法:讓進程回退到足以避免死鎖的地步

例題解析

  • Leetcode 1226題, 哲學家進餐

  • 5個沉默寡言的哲學家圍坐在圓桌前,每人面前一盤意面。叉子放在哲學家之間的桌面上。(5 個哲學家,5 根叉子)

    所有的哲學家都只會在思考和進餐兩種行為間交替。哲學家只有同時拿到左邊和右邊的叉子才能吃到面,而同一根叉子在同一時間只能被一個哲學家使用。每個哲學家吃完面后都需要把叉子放回桌面以供其他哲學家吃面。只要條件允許,哲學家可以拿起左邊或者右邊的叉子,但在沒有同時拿到左右叉子時不能進食。

    假設面的數量沒有限制,哲學家也能隨便吃,不需要考慮吃不吃得下。

    設計一個進餐規則(並行算法)使得每個哲學家都不會挨餓;也就是說,在沒有人知道別人什么時候想吃東西或思考的情況下,每個哲學家都可以在吃飯和思考之間一直交替下去。

    img

  • 哲學家從 0 到 4 按 順時針 編號。請實現函數 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

    • philosopher 哲學家的編號。
    • pickLeftForkpickRightFork 表示拿起左邊或右邊的叉子。
    • eat 表示吃面。
    • putLeftForkpickRightFork 表示放下左邊或右邊的叉子。
    • 由於哲學家不是在吃面就是在想着啥時候吃面,所以思考這個方法沒有對應的回調。
  • 給你 5 個線程,每個都代表一個哲學家,請你使用類的同一個對象來模擬這個過程。

  • 在最后一次調用結束之前,可能會為同一個哲學家多次調用該函數。

  • 示例:

    輸入:n = 1
    輸出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
    解釋:
    n 表示每個哲學家需要進餐的次數。
    輸出數組描述了叉子的控制和進餐的調用,它的格式如下:
    output[i] = [a, b, c] (3個整數)
    - a 哲學家編號。
    - b 指定叉子:{1 : 左邊, 2 : 右邊}.
    - c 指定行為:{1 : 拿起, 2 : 放下, 3 : 吃面}。
    如 [4,2,1] 表示 4 號哲學家拿起了右邊的叉子。 
    
    • 解法一

      package com.ronnie.leetcode;
      
      import java.util.concurrent.Semaphore;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class DiningPhilosophers {
      
          // 1 fork => 1 ReetrantLock
          private ReentrantLock[] lockList = {
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock()
          };
          // restriction: Only 4 Philosophers can get at least one fork
          private Semaphore eatLimit = new Semaphore(4);
      
          public DiningPhilosophers() {
      
          }
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
      
              int leftFork = (philosopher+1)%5;
              int rightFork = philosopher;
              eatLimit.acquire();
      
              lockList[leftFork].lock();
              lockList[rightFork].lock();
      
              pickLeftFork.run();
              pickRightFork.run();
      
              eat.run();
      
              putLeftFork.run();
              putRightFork.run();
      
              lockList[leftFork].unlock();
              lockList[rightFork].unlock();
      
              eatLimit.release();
          }
      }
      
    • 解法二

      package com.ronnie.leetcode;
      
      import java.util.concurrent.locks.ReentrantLock;
      
      public class DiningPhilosophers2 {
      
          // 1 fork => 1 ReentrantLock
          private ReentrantLock[] lockList = {
                new ReentrantLock(),
                new ReentrantLock(),
                new ReentrantLock(),
                new ReentrantLock()
          };
      
          // 思路: 設置一個臨界區, 進入臨界區后, 只有當哲學家獲取到左右兩把叉子並執行代碼后, 才退出臨界區
          private ReentrantLock pickBothForks = new ReentrantLock();
      
          public DiningPhilosophers2(){
      
          }
      
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
      
                      int leftFork = (philosopher + 1) % 5;
                      int rightFork = philosopher;
      
                      // 進入臨界區
                      lockList[leftFork].lock();
                      lockList[rightFork].lock();
      
                      pickLeftFork.run();
                      pickRightFork.run();
      
                      // 退出臨界區
                      pickBothForks.unlock();
      
                      putLeftFork.run();
                      pickRightFork.run();
      
                      lockList[leftFork].unlock();
                      lockList[rightFork].unlock();
          }
      }
      
    • 解法三

      package com.ronnie.leetcode;
      
      import java.util.concurrent.locks.ReentrantLock;
      
      /**
       *  只有當5個哲學家都左手持其左邊的叉子 或 都右手持有期右邊的叉子時, 才會發送死鎖。
       */
      public class DiningPhilosophers3 {
          // 1 fork => 1 ReetrantLock
          private ReentrantLock[] lockList = {
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock()
          };
      
          public DiningPhilosophers3() {
          }
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
              int leftFork = (philosopher + 1) % 5;
              int rightFork = philosopher;
      
              // 讓編號為偶數的哲學家優先拿左邊, 再拿右邊
              if (philosopher % 2 == 0){
                  lockList[leftFork].lock();
                  lockList[rightFork].lock();
              } else {
                  // 編號為奇數的舊先拿右邊再拿左邊, 這樣就避免了死鎖
                  lockList[rightFork].lock();
                  lockList[leftFork].lock();
              }
      
              pickLeftFork.run();
              pickRightFork.run();
      
              eat.run();
      
              putLeftFork.run();
              putRightFork.run();
      
              lockList[leftFork].unlock();
              lockList[rightFork].unlock();
          }
      }
      
      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM