モダンOSのお砂場(21) FreeRTOS、Mutex排他制御の効果が「分かる」サンプル

Joseph Halfmoon

今回はRTOSでは避けて通れない排他制御を実験してみます。それも「排他制御無」だとこんなに動作が「ボロボロだ」と実感できるサンプルを作ってそこに排他制御(mutex)を導入し、「解決できた」ところを見ようという野望です。タイミングとかいろいろあるし、そんなに上手くサンプルプログラムを作れるのか。

※「モダンOSのお砂場」投稿順Indexはこちら

(今回使用のソースコードの全文は末尾に掲げてあります)

さて今回利用してみようとしている排他制御のためのオブジェクトは mutex です。共有リソースを「使いたい人」が複数いるとき、瞬間的には「おひとり様づつ」お使いいただくための「あれ」です。Windowsでマルチスレッドのプログラムを書くときにも御馴染みじゃないかと思います。ただし、例によってプログラムするハードウエアは Espressif Systems社のESP32 DevKitCボード、ソフトウエア開発環境はArduino IDE、そしてOSは FreeRTOS です。

FreeRTOSでの mutex 関係のAPIは、兄弟分?の Semaphore と一緒のページに書かれておりました。そこへのURLは下に。

FreeRTOS Semaphore and mutex

上記を読めばOKっという感じですが、例によって Shawn Hymel先生が、Digi-Key社のYouTubeチャンネルで分かり易いビデオをアップロードされているのでそこへのリンクも貼り付けておきます。

Introduction to RTOS Part 6 – Mutex | Digi-Key Electronics

排他制御しないとボロボロになるサンプルプログラム

普段「動いている」ように見えるのに、稀に「バグる」と分けわからなくて困る事が多い者共の中に「排他制御」もいらっしゃるように思います。いつも同じ挙動を示すのなら再現して原因究明するのも簡単なのだけど、ちょっと条件変わると挙動が変わってなかなか捕まえられない。そんな感じのサンプルプログラムを作れたら良いな、と思いました。しかし、そういう困った人達を、意図して「バグらせよう」とすると、思ったような挙動を示さないのだ、これが。

今回、「ボロボロな挙動として」狙ったところは以下です。

  1. ありがちな設定、2タスクが共有リソースをインクリメント
  2. 共有リソースは2次元、バグると思わぬところをインクリメントするかも
  3. 同じところで2タスクが競合するとカウント値が狂うこともあり
  4. 2タスクの共有リソースへのアクセスタイミングはバラつかせる
  5. そのバラツキの大きさで挙動がどう変わるのか見えるようにする

そして、mutex による排他制御により

  • 上記の問題が解決されることを確認
  • しかし、排他制御により「待ち」が発生する様子も観察

結構盛、沢山です。末尾のコード、冒頭の

#define USE_MUTEX

をコメントアウトして実行した場合の挙動を以下に示します。

ここで waitMAX は、乱数で発生させているタイミングのバラツキのMAX値[ms]です。1mから5mまで振ってその様子を見ています。タスクt1 と タスクt2 が共有リソースにアクセスしようとしており、そのアクセスを試みた回数を t1Count, t2Countで数えています。ここでは排他制御が無いので、毎回必ずアクセスしています。その下の5行x5列の配列が共有リソースです。「設計上」は対角線の項にのみカウントが入り、その他は0になるべきなのですが、排他制御がバグると対角線以外の項を操作してしまいます。さきほどのバラツキMAX値によって随分挙動が変わります。その下の sum はインクリメントした回数です。ここでは、t1Count + t2Count == sum なる関係であればカウント漏れは無い、ということになります。それ以外は良からぬことが起こっています。notTAKEは後で mutex 使った排他制御で見るカウント値なのでここでは意味がありません。

排他制御なし「ボロボロ」な実行結果
Without Mutex
------------------------------
waitMAX: 1
t1Count: 1051
t2Count: 1050
00211 00000 00000 00000 00210 
00210 00210 00000 00000 00000 
00000 00210 00210 00000 00000 
00000 00000 00210 00210 00000 
00000 00000 00000 00210 00210 
sum: 2101
notTAKE: 0
------------------------------
waitMAX: 2
t1Count: 1050
t2Count: 1050
00001 00002 00000 00000 00000 
00000 00002 00002 00000 00000 
00000 00000 00001 00002 00000 
00000 00000 00000 00001 00002 
00002 00000 00000 00000 00001 
sum: 16
notTAKE: 0
------------------------------
waitMAX: 3
t1Count: 1023
t2Count: 1023
00205 00000 00000 00000 00204 
00205 00205 00000 00000 00000 
00000 00205 00205 00000 00000 
00000 00000 00204 00204 00000 
00000 00000 00000 00204 00204 
sum: 2045
notTAKE: 0
------------------------------
waitMAX: 4
t1Count: 1015
t2Count: 1035
00080 00071 00108 00063 00081 
00082 00078 00072 00110 00069 
00061 00076 00106 00105 00062 
00063 00105 00077 00113 00114 
00071 00068 00091 00112 00103 
sum: 2141
notTAKE: 0
------------------------------
waitMAX: 5
t1Count: 1007
t2Count: 1011
00084 00083 00082 00064 00082 
00057 00062 00061 00058 00063 
00066 00063 00079 00062 00084 
00084 00059 00079 00056 00085 
00064 00085 00083 00056 00084 
sum: 1785
notTAKE: 0

上記の「ボロボロ」な実行結果をまとめると以下のようです。

  1. waitMAX=1のとき、対角線の隣接項をインクリメントすることがある。インクリメントの回数は正しい。
  2. waitMAX=2のとき、インクリメント回数異常に少ない。対角線の隣接項をインクリメントすることがある。
  3. waitMAX=3のとき、対角線の隣接項をインクリメントすることがある。インクリメントの回数が微妙に(-1)少ない。
  4. waitMAX=4のとき、全ての項をインクリメントしてしまっている。インクリメント回数91回も多い。
  5. waitMAX=5のとき、全ての項をインクリメントしてしまっている。インクリメント回数200回以上も少ない。

待ち時間を「ちょっと」変更するだけで「バグの見え方」が大きく異なることが分かります。それぞれの「バグる」メカニズムを追うのも楽しいかと一瞬思いましたが、今回テーマは mutex なので流します。

mutex使って排他制御した場合。

さきほどコメントアウトしていたマクロを define すれば mutex を使って排他制御するようになります。この場合期待される挙動は、

  1. 対角線の項のみインクリメントされる。その他はゼロ
  2. インクリメント回数は t1Count + t2Count == sum + notTAKE という関係になる

ということです。

ここで、notTAKEは、mutex を取りに行ったのだけれど、他方が既に取得済、そこで所定の時間だけ開放を待ってみたけれども取得できなかった、という場合の回数です。t1Countもt2Countも取りに行けば取得の可否に関わらずカウントアップしているので、駄目だったときはここに値が入ります。通常、mutexを確保できなかった場合、時間的に処理を待てるのであれば、何か別の処理をやって時間潰してから再トライしてみる。待てないのであれば、リソース使えなかった「万歳」タイムアウト・エラーでしょう。このサンプルプログラムで「所定の時間」というのは以下で定義されている値です。

#define TICK_TO_WAIT (10)

当然、この時間の長さを操作することで挙動が変わります。また、先ほども出ている waitMAX 値にも依存するのは言うまでもありません。notTAKEにカウントが入るということは、mutex 取得待ちのオーバヘッドの大きさを間接的に示していることになると考えます。

排他制御(Mutex)を行ったときの実行結果
With Mutex
------------------------------
waitMAX: 1
t1Count: 1018
t2Count: 1017
00408 00000 00000 00000 00000 
00000 00408 00000 00000 00000 
00000 00000 00407 00000 00000 
00000 00000 00000 00406 00000 
00000 00000 00000 00000 00406 
sum: 2035
notTAKE: 0
------------------------------
waitMAX: 2
t1Count: 1017
t2Count: 1018
00407 00000 00000 00000 00000 
00000 00408 00000 00000 00000 
00000 00000 00408 00000 00000 
00000 00000 00000 00406 00000 
00000 00000 00000 00000 00406 
sum: 2035
notTAKE: 0
------------------------------
waitMAX: 3
t1Count: 1002
t2Count: 1002
00400 00000 00000 00000 00000 
00000 00401 00000 00000 00000 
00000 00000 00401 00000 00000 
00000 00000 00000 00401 00000 
00000 00000 00000 00000 00401 
sum: 2004
notTAKE: 0
------------------------------
waitMAX: 4
t1Count: 1003
t2Count: 1003
00401 00000 00000 00000 00000 
00000 00402 00000 00000 00000 
00000 00000 00402 00000 00000 
00000 00000 00000 00401 00000 
00000 00000 00000 00000 00400 
sum: 2006
notTAKE: 0
------------------------------
waitMAX: 5
t1Count: 1011
t2Count: 1011
00381 00000 00000 00000 00000 
00000 00372 00000 00000 00000 
00000 00000 00374 00000 00000 
00000 00000 00000 00378 00000 
00000 00000 00000 00000 00381 
sum: 1886
notTAKE: 136

mutexにより排他制御された実行結果をまとめると以下のようです。

  1. waitMAX=1のとき、対角線の隣接項のみインクリメントされている。回数も正しい。notTAKEもゼロ。
  2. waitMAX=2のとき、対角線の隣接項のみインクリメントされている。回数も正しい。notTAKEもゼロ。
  3. waitMAX=3のとき、対角線の隣接項のみインクリメントされている。回数も正しい。notTAKEもゼロ。
  4. waitMAX=4のとき、対角線の隣接項のみインクリメントされている。回数も正しい。notTAKEもゼロ。
  5. waitMAX=5のとき、対角線の隣接項のみインクリメントされている。回数も正しいがnotTAKEは136もある。

「ボロボロ」のときのバグはFIXされました。注目するのは、待ち時間のバラツキがある値を超えると急に mutex 待ちの回数が増えているように見えることです。当然、タイムアウト待ち時間の設定値依存だと思います。排他制御の性質上、致し方ないとも言えますが、このままここの値がもっと増えると、マルチタスクで処理する意味が無くなるかもしれませぬ。

バグ実行も正常実行も微妙な例だわな。

モダンOSのお砂場(20) FreeRTOS、キュー構造。でも別件が気になってしまうっ!へ戻る

モダンOSのお砂場(22) FreeRTOS、ソフトウエアタイマAPIでワンショット へ進む

Mutexの効果が確かめられるサンプルプログラム
#define USE_MUTEX
#define ASIZE (5)
#define MAXTRIAL (1000)
#define MAXEND (5)
#define TICK_TO_WAIT (10)

static const BaseType_t app_cpu = 0;
static const int led_pin = 23;
static volatile bool goFlag = false;

static int xPOS;
static int yPOS;
static int sharedArray[ASIZE][ASIZE];
static int t1POS;
static int t1Count;
static int t2POS;
static int t2Count;
static int waitMIN;
static int waitMAX;
static int notTAKE;

static SemaphoreHandle_t mutex;

void wasteTime(void) {
  vTaskDelay(random(waitMIN, waitMAX));
}

void accessArray(int x, int y) {
  int temp;
#ifdef USE_MUTEX
  if (xSemaphoreTake(mutex, TICK_TO_WAIT) == pdTRUE) {
#endif
    xPOS = x;
    wasteTime();
    yPOS = y;
    temp = sharedArray[xPOS][yPOS];
    temp++;
    wasteTime();
    sharedArray[xPOS][yPOS] = temp;
#ifdef USE_MUTEX
    xSemaphoreGive(mutex);
  } else {
    notTAKE++;
  }
#endif
}

void t1(void *params) {
  while (1) {
    if (goFlag) {
      accessArray(t1POS, t1POS);
      t1Count++;
    }
    t1POS++;
    if (t1POS >= ASIZE) {
      t1POS = 0;
    }
  }
}

void t2(void *params) {
  while (1) {
    if (goFlag) {
      accessArray(t2POS, t2POS);
      t2Count++;
    }
    t2POS++;
    if (t2POS >= ASIZE) {
      t2POS = 0;
    }
  }
}

void clearCounters(void) {
  for (int i=0; i<ASIZE; i++) {
    for (int j=0; j<ASIZE; j++) {
      sharedArray[i][j]=0;
    }
  }
  t1POS = 0;
  t1Count = 0;
  t2POS = 0;
  t2Count = 0;  
  notTAKE = 0;
}

void setup() {
  randomSeed(123); //Use same sequence for verify.
  Serial.begin(115200);
  while(!Serial) {};
  pinMode(led_pin, OUTPUT);
  delay(10000);
#ifdef USE_MUTEX
  Serial.println("With Mutex");
  mutex = xSemaphoreCreateMutex();
#else
  Serial.println("Without Mutex");
#endif
  
  clearCounters();

  waitMIN = 1;
  waitMAX = 1;
  xTaskCreatePinnedToCore(t1, "t1", 1024, NULL, 0, NULL, app_cpu);
  xTaskCreatePinnedToCore(t2, "t2", 1024, NULL, 0, NULL, app_cpu);
  goFlag = true;
}

void loop() {
  if (goFlag && (t1Count > MAXTRIAL) && (t2Count > MAXTRIAL)) {
    int sum = 0;
    goFlag = false;
    vTaskDelay(100);
    Serial.println("------------------------------"); 
    Serial.print("waitMAX: ");
    Serial.println(waitMAX); 
    Serial.print("t1Count: ");
    Serial.println(t1Count); 
    Serial.print("t2Count: ");
    Serial.println(t2Count); 
    for (int i=0; i<ASIZE; i++) {
      for (int j=0; j<ASIZE; j++) {
        Serial.printf("%05d ",sharedArray[i][j]);
        sum += sharedArray[i][j];
      }
      Serial.println(); 
    }
    Serial.print("sum: ");
    Serial.println(sum); 
    Serial.print("notTAKE: ");
    Serial.println(notTAKE); 
  }
  if (goFlag) {
    digitalWrite(led_pin, HIGH);
    vTaskDelay(100);
    digitalWrite(led_pin, LOW);
    vTaskDelay(100);
  } else {
    if (waitMAX < MAXEND) {
      waitMAX++;
      clearCounters();
      goFlag = true;
    } else {
      vTaskDelay(10000);
    }
  }
}