排他制御の仕組みと効果・共有データには排他制御を掛けましょう

2022年6月20日その他

マルチタスクの環境で使う共有データには注意が必要

マルチタクスの環境では複数のタスクで共通にアクセスのできる共有データを使う事がよくあります。共有データはとて便利な仕組みですが、必要な排他制御を忘れるデータの書き壊しや不正データの参照が起きてしまいます。

この記事では、共通データを利用する時に必要な排他制御とは何なのか? どんな仕組みで共有データの書き壊しや不正データの参照を防止ししているのか、について簡単に紹介します。

なお、排他制御に関わるソフトのバグについては、別の記事で紹介する予定ですので、興味のある方はそちらもご覧ください。

排他制御とはある一定の期間にデータのアクセスを独占する仕組み

排他制御とは「他のタスク」が特定のデータにアクセスする事を「排徐」して、自分だけがそのデータのアクセスを独占するための仕組みです、他のタスクを排除するので略して排他です。もう少し言葉を補うと、排他制御とはマルチタスクの実行環境で提供されている共有データの特定の部分へのアクセスに対して、一定の期間は自タスクだけが書き込みや読み出し等のアクセスができるようにするため、他のタスクによるアクセスを排除するための仕組みです。

排他制御のこの仕組みを実装するために、マルチタスクのOSはセマフォやミューテクスと呼ばれる、排他制御のための機能を持ったデータ構造を提供しています。セマフォはカンター方式、ミューテクスは On/Off のフラグ形式という違いはありますが、その機能は殆ど同じです。ここでは、動作が簡単なミューテクスを使って説明します。

ミューテックスはそれを確保したタスク以外は休止させる機能を持っている

ミューテクスは On/Off というフラグ形式の排他制御のためのデータ構造で、あるミューテクス例えば Mut-A1 が Onの状態を持てるタスクはある一瞬には1つのみです。この Mut-A1 が On の状態を持つ事を、ミューテックス Mut-A1 を獲得している、と言います。

もし、他あるタスク Task-Z1 がミューテクス Mut-A1 を既に獲得している時に、別のタスク Task-X1 が同じミューテクス Mut-A1 の 獲得を要求すると、タスク Task-X1 はOSにより休止状態に置かれます。そして、タスク Task-Z1 が ミューテックス Mut-A1 を解放(Mut-A1 を Off にした)した時点で、このタスク Tazk-X1 は休止状態が解除されます。

このミューテクスの解放までタスクが休止状態にされる操作は、タスクの優先順位とは関係がなく、先にミューテクスを獲得したタスクがあれば、そのミューテクスが解放されるまでは他のタスクは優先順位に関係なく休止状態にされます。

ミューテックスを共有データの特定の部分と対応付ける事でアクセス制御をする

排他制御に使うデータ構造のミューテックスには、上記のようにタスクを休止状態にして実行を止める機能があるので、この機能を使って特定の共有データについてのアクセスを独占するように「コードを作成する」事が可能になります。

そのため必要なのは、以下のような「コーディング」です。

① 排他制御が必要となる共有データの部分を決める

② ①で決めたした共有データを排他制御するために使うミューテックスを決める

③ ①で決めた共有データをアクセスする時には事前に②で決めたミューテックスを獲得する

④ ①で決めた共有データのアクセスを終えたら③で獲得したミューテックスを解放する

排他制御が必要な共有データをアクセスする全てのタスクで上記のようなコーディングを行う事で、共有データへのアクセスを「その時点でミューテックスを獲得しているタスクに独占させる」事ができる様になり、排他制御が実現できます。

排他制御が無いと何が起きるのかソフトの動きを確認してましょう

ちょっと判り難いので、少し具体的にソフトの動きを追いかけてみましょう。例として、果物の出荷場で出荷箱に何が入っているのかを管理するソフトを考えてみましょう。

出荷箱に入っている物を管理するためのデータ構造として、品名、個数、単価の3つのメンバーを持つ出荷箱構造体を使います。 出荷箱構造体.品名=リンゴ、出荷箱構造体.個数=20個、出荷箱構造体.単価=100円 という感じですね。そして、この出荷箱構造体をアクセスして値を書き換えるタスクとして、箱詰めタスクと再確認タスクの2つがあって、優先度は再確認タスク>箱詰めタスク と仮定しましょう。

まずは排他制御の仕組みを入れずにソースコードを作った場合に、箱図詰めタスクと再確認タスクの実行タイミングが重なると何が起きるかを確認してみます。ここで、箱詰めタスクは出荷箱構造体の値を 品名=リンゴ、個数=20個、単価=100円 に設定しようとしていて、再確認タスクは出荷箱構造体の値を 品名=メロン、個数=5個、単価=1500円 に設定しようとしているとします。

2つのタスクの実行タイミングが重ならないが時には、後で実行されたタスクの設定した値が残ります。例えば再確認タスクが後で実行されれば 出荷箱構造体 の値は 品名=メロン、個数= 5個、単価 =1500円 となり、箱詰めタスクが後で実行されれば出荷箱構造体の値は品名=リンゴ、個数=20個、単価=100円 となります。しかし、箱図詰めたすくと再確認タスクの実行タイミングが重なってしまうと、ちょっと困った事が起きます。どんな事が起きるか順番に見てきましょう。

1) 箱詰めタスクが出荷箱構造体の値の変更を開始し、出荷箱構造体.品名=リンゴ、出荷箱構造体.個数=20個 と書く

2) そのタイミングで優先度の高い再設定タスクが起動すると、箔詰めタスクは実行を中断される

3) 再確認タスクは、出荷箱構造体.品名=メロン、出荷箱構造体.個数=5個、出荷箱構造体.単価=1500円 と書く

4) 再確認タスクの実行が終わり箱詰めタスクが実行を再開して、出荷箱構造体.単価=100円 と書く

上記のソフトの実行では、箱詰めタスクも再確認タスクもそれぞれの実行は正常に終了しています。なのに、出荷箱構造体の値は、品名=メロン、個数=5個、単価=100円 と、リンゴとメロンの値が入り混じった、構造体のメンバ間のデータの整合性が崩れたおかしな値になってしまっています。これが、排他制御のミス(排他制御の実装忘れもミスのひとつです)で引き起こされる、共有データの書き壊しです。

排他制御をしているとどんな仕組みで書き壊しが防がれるのか?

では、この出荷箱構造体に対して排他制御の仕組みを組み込んでいれば、結果はどうなるのでしょうか? 箱詰めタスクと再確認タスクの両方に、 Mut-Box というミューテックスを使って箱詰め構造体の排他制御を組み込んであった場合のソフトの動きを追いかけてみましょう。

0) 箱詰めタスクは、出荷箱構造体の値の変更をするために Mut-Box ミューテックスを獲得する。

1) 箱詰めタスクが出荷箱構造体.品名=リンゴ、出荷箱構造体.個数=20個 と書く

2) そのタイミングで優先度の高い再設定タスクが起動する。

3) 再設定タスクは、出荷箱構造体の更新のために Mut-Box ミューテックスを獲得しようとするが、既に箱詰めタスクが Mut-Box ミューテックス を獲得しているために再設定タスクは休止状態になる。

3) 箱詰めタスクは実行を続けて出荷箱構造体.単価=100円 と書く。

4) 箱詰めタスクは出荷箱構造体の更新を終えたので Mut-Box ミューテックスを解放する。

5) この時点で OS により再確認タスクの休止状態が解除され、優先度の高い再確認タスクの実行が再開される。

6) 実行を再開した再確認タスクは Mut-Box ミューテックス  の獲得に成功して出荷箱構造体の更新に進む

7) 再確認タスクは、出荷箱構造体.品名=メロン、出荷箱構造体.個数=5個、出荷箱構造体.単価=1500円 と書く

8) 再確認タスクは、出荷箱構造体の更新を終えたので Mut-Box ミューテックスを解放する。

この結果、出荷箱構造体の値は品名=メロン、個数=5個、単価=1500円 と、品名・個数・単価 というデータ間の関連性が維持された状態を保って更新されます。 これが、排他制御による共有データの書き壊しの防止です。

複数タスクでの書き込みで起きる書き壊し

・例えば果物出荷管理のソフトで出荷箱の中身を管理する出荷箱構造体を考える。品名、個数、単価のメンバを持つ

 ・出荷箱構造体:品名:リンゴ、個数:20個、単価 100円

・出荷箱構造体は、箱詰めタスクと確認タスクの2つのタスクが値を更新する事ができ、確認タスクが優先度が高いとする。

・箱詰めタスクは、出荷箱構造体の内容を品名:リンゴ、個数 20個、単価 100円 と設定しようとしており

・確認タスクは、出荷箱[1] の愛用を、品名:メロン、個数 5個、単価 1500円 と設定しようとしている。

・普通は、後で実行された方の値が残るので、箱詰めタスクが後で実行されれば 出荷箱[1] の内容は リンゴ、20個、単価 100円 となり、更新タスクが後で実行されれば出荷箱[1] の内容はメロン、5個、単価 1500円 となる。

・しかし、箱詰めタスクと確認タスクがほぼ同時に実行されると、以下の様な事が起きる

 ① 箱詰めタスクが少し早く実行を開始し、品名=リンゴ、個数=20個 までデータを書き込んだ時点で

 ② 確認タスクが実行され、優先度が高いので実行権を獲得して、品名=メロン、個数= 5個、単価=1500円 と値を更新して終了し

 ③ 確認タスクの終了により箱詰めタスクが作業を再開して 単価=100円 を書き込む。

 ④ その結果、出荷箱[1]の内容は、品名=メロン、個数=5個、単価 100円  となり、激安メロンの出荷となってしまう。

データの更新が1つのタスクだけの場合でも排他制御が必要

先ほどの例では出荷箱構造体の更新を2つのタスクが同じタイミングで実行していたので、排他制御が無いとデータの書き壊しが起きていました。 それでは、データの更新を1つのタスクだけが行う場合には、排他制御は不要でしょうか? 実は、データの更新が1つのタスクに限定されている場合でも、排他制御が実装されていないと、そのデータを参照する他のタスクが整合性の崩れたデータを読みだしてしまう場合があるので、排他制御は必要になります。

先ほどと同じく、出荷箱構造体を例に考えます。今度は、再確認タスクは存在しなくて、箱詰めタスクが出荷箱構造体の値を更新(書き込み)し、出荷額計算タスクが出荷箱構造体の値を参照(読出し)するだけとします。 タスクの優先度は 箱詰めタスク>出荷額計算タスク とします。 先ほどと同じように、排他制御が無い場合に、2つのタスクの実行タイミングが重なった時を想定して、ソフトの動きを確認してみましょう。

1) 出荷額計算タスクが出荷箱構造体の参照を開始し、出荷箱構造体.品名=リンゴ、出荷箱構造体.個数=20個 と読む

2) そのタイミングで優先度の高い箱詰めタスクが起動すると、出荷額計算タスクは実行を中断される

3) 箱詰めタスクは、新たな値として出荷箱構造体.品名=マンゴー、出荷箱構造体.個数=15個、出荷箱構造体.単価=500円 と書く

4) 箱詰めタスクの実行が終わり出荷額計算タスクが実行を再開して、出荷箱構造体.単価=500円 と読む

この段階で、出荷額計算タスクは 品名=リンゴ、個数=20個、単価=500円 と値を読み出しているので、出荷額の計算値がおかしな事になります。これが、品名、個数、単価 という整合性の必要な一連の共有データに対して、正しい排他制御が実装されていなかった事による、不整合なデータ参照の発生です。

この場合でも、箱詰めたすくと出荷額計算タスクとで、出荷箱構造体のアクセルに対する排他制御が実装されていれば、データの整合性が崩れる事はありません。

データ間の整合性が必要な共有データには排他制御を忘れないようにしましょう

このように、2つのタスクが値を更新する場合だけではなく、1つのタスクが値を更新して他のタスクがその値を参照するだけの場合であっても、整合性の必要なデータのアクセスには排他制御が必要となるので、注意しましょう。