FreeRTOS理解その13（デッドロック）

## デッドロック
今回のお題は、FreeRTOS上でデッドロックを故意に発生させるもの。Arduino IDE環境下で、ESP32搭載のM5Stackを使う。

### 方針
2つのタスクが2つの資源（LCDおよびシリアル回線）を利用し、これらの資源の排他制御が不適切な例を考える。2つのタスクは、M5Stackの左ボタン押下時に、LCD表示及びシリアル経由でPCへの表示を行うタスク、右ボタン押下時に同様のことを行うタスクである。また、表示内容はボタンが押された回数とする。

## その前に（排他制御せず）
デッドロック以前に、排他制御自体を行わないと、どうなるかをトライ。

```c++:NoExclusion.cpp
#include <M5Stack.h>

#define L_BTN 39
#define R_BTN 37

uint32_t      l_btn_cnt;
uint32_t      r_btn_cnt;
xTaskHandle   xTaskL, xTaskR;

void IRAM_ATTR l_btn_intr() {
  l_btn_cnt++;
}

void IRAM_ATTR r_btn_intr() {
  r_btn_cnt++;
}

void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt);
      Serial.println(buf);
      M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
      M5.Lcd.drawString(buf, 20, 20);
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt);
      Serial.println(buf);
      M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
      M5.Lcd.drawString(buf, 20, 60);
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  M5.begin();
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);  
  pinMode(L_BTN, INPUT);
  pinMode(R_BTN, INPUT);
  attachInterrupt(digitalPinToInterrupt(L_BTN), l_btn_intr, FALLING);
  attachInterrupt(digitalPinToInterrupt(R_BTN), r_btn_intr, FALLING);
  l_btn_cnt = 0;
  r_btn_cnt = 0;
  Serial.println("Task Start");
  xTaskCreate(&TaskL, "TaskL", 1024, NULL, 1, &xTaskL);
  xTaskCreate(&TaskR, "TaskR", 1024, NULL, 1, &xTaskR);
}

void loop() {
  vTaskDelay(1);
}
```

左ボタン押下用のTaskLと右ボタン押下用のTaskRとが存在し、それぞれのボタン押下時のハンドラl_btn_intr()とr_btn_intr()とでは、ボタン押下のたびにカウントをインクリメントする。

これを実行し、両方のボタン押下を連続的に続けていくと、リブートしてしまう、、、。
![排他制御なし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/100a8eb8-a948-f7c7-0893-21e0b2eefd5b.png)

LCD表示ではこんな感じ。（ピンぼけすみません。）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/1cde9183-20af-c189-5876-79b8c3174cd9.png", width=60%>

## デッドロック１（不適切な排他制御（資源の確保））
LCDおよびシリアル回線利用のための排他制御を入れるが、あえてデッドロックするような制御をおこなう。

TaskLでは、シリアル回線資源が確保できたとき<B><I>のみ</I></B>、LCD資源の確保を実施し、TaskRでは、LCD資源が確保できたとき<B><I>のみ</I></B>、シリアル回線資源の確保、、、を実施する。これにより、TaskLではシリアル回線資源確保後のLCDの解放待ち、TaskRではLCD資源確保後のシリアル回線資源の解放待ち、、、という状況を比較的簡単に作ることができる（ボタン押下のタイミング次第ではあるが）。

```c++:DeadLock1.cpp
#include <M5Stack.h>

#define L_BTN 39
#define R_BTN 37

uint32_t      l_btn_cnt;
uint32_t      r_btn_cnt;
xTaskHandle   xTaskL, xTaskR;
SemaphoreHandle_t xSemSerial, xSemLCD;

void IRAM_ATTR l_btn_intr() {
  l_btn_cnt++;
}

void IRAM_ATTR r_btn_intr() {
  r_btn_cnt++;
}

void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
          M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
          M5.Lcd.drawString(buf, 20, 20);
          xSemaphoreGive(xSemLCD);          
        }
        xSemaphoreGive(xSemSerial);
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
          Serial.println(buf);
          xSemaphoreGive(xSemSerial);        
        }
        xSemaphoreGive(xSemLCD);
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  M5.begin();
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);  
  pinMode(L_BTN, INPUT);
  pinMode(R_BTN, INPUT);
  attachInterrupt(digitalPinToInterrupt(L_BTN), l_btn_intr, FALLING);
  attachInterrupt(digitalPinToInterrupt(R_BTN), r_btn_intr, FALLING);
  l_btn_cnt = 0;
  r_btn_cnt = 0;
  Serial.println("Task Start");
  xTaskCreate(&TaskL, "TaskL", 1024, NULL, 1, &xTaskL);
  xTaskCreate(&TaskR, "TaskR", 1024, NULL, 1, &xTaskR);
  xSemSerial = xSemaphoreCreateMutex();
  xSemaphoreGive(xSemSerial);  
  xSemLCD = xSemaphoreCreateMutex();
  xSemaphoreGive(xSemLCD);  
}

void loop() {
  vTaskDelay(1);
}

```

ここでは、排他制御にはセマフォを利用。xSemaphoreCreateMutex()によるセマフォ作成、xSemaphoreTake()による資源確保、xSemaphoreGive()による資源開放が行われる。最初のxSemaphoreTake()成功時のみ、次のxSemaphoreTake()を呼び出しているところがミソ。

デッドロック中のLCD表示。（ピンぼけすみません。）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/fe71ed3d-1c49-15cd-f7fd-d4dcc35ec59f.png", width=60%>

その時のPC側の表示及び約3時間後の強制リブート（M5 Stack赤ボタン押下）の様子。
![デッドロック後リブート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/74a35d02-26d0-a87a-274b-d520062255c0.png)

## デッドロック２
TaskL()およびTaskR()のみ記載。その他のコードは、デッドロック１と同じ。

```c++:DeadLock2.cpp
void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);
      }
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 20);
        xSemaphoreGive(xSemLCD);                  
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        xSemaphoreGive(xSemLCD);
      }
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);        
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}

```

TaskLではシリアル回線⇒LCDの順で資源確保、TaskRではLCD⇒シリアル回線の順で資源確保を行っている。TaskLでのLCDの資源確保待ち、TaskRでのシリアル回線の資源確保待ちということがありうるが、それぞれのタスクで最初の資源確保後、次の資源確保までにタスクスイッチが行われずに、このコードでデッドロックを起こすことはできなかった。

## デッドロック３
xSemaphoreTake()の成功したときに、vTaskDelay()をコールすれば、タスクスイッチが行われる。例えば、TaskRがLCD資源を確保中に、TaskLがLCD資源確保を行う状況を作り出せるのではないか、、と考えた。ここでも、TaskL()およびTaskR()のみ記載。

```c++:DeadLock3.cpp
void TaskL(void *arg) {
  uint32_t old_cnt = l_btn_cnt;
  char buf[16];
  while (1) {
    if (l_btn_cnt != old_cnt) {
      sprintf(buf, "TaskL %d", l_btn_cnt); // \n
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        vTaskDelay(1);　　// ここでタスクスイッチ！
        xSemaphoreGive(xSemSerial);
      }
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 20, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 20);
        xSemaphoreGive(xSemLCD);                  
      }
      old_cnt = l_btn_cnt;
    }
    vTaskDelay(1);
  }
}

void TaskR(void *arg) {
  uint32_t old_cnt = r_btn_cnt;
  char buf[16];
  while (1) {
    if (r_btn_cnt != old_cnt) {
      sprintf(buf, "TaskR %d", r_btn_cnt); // \n
      if (xSemaphoreTake(xSemLCD, portMAX_DELAY) == pdTRUE) {
        M5.Lcd.fillRect(20, 60, 200, 40, BLACK);
        M5.Lcd.drawString(buf, 20, 60);
        vTaskDelay(1);　　// ここでタスクスイッチ！
        xSemaphoreGive(xSemLCD);
      }
      if (xSemaphoreTake(xSemSerial, portMAX_DELAY) == pdTRUE) {
        Serial.println(buf);
        xSemaphoreGive(xSemSerial);        
      }
      old_cnt = r_btn_cnt;
    }
    vTaskDelay(1);
  }
}
```

論理的にはデッドロックが発生しえるが、このコードでも発生させることはできなかった。ボタン押下のタイミングが影響しているのであろう。

## おわりに
故意にデッドロック起こすのにも頭を使う。