コンテンツにスキップ

リエントラント

出典: フリー百科事典『ウィキペディア(Wikipedia)』

リエントラント: reentrant / re-entrant再入可能)およびリエントランシー: reentrancy / re-entrancy再入可能性)とは、あるプログラムサブルーチンの実行を完了する前に、割り込みなどにより、同じプログラムやサブルーチンを実行しても安全だという性質を指す。割り込みは分岐や呼び出しなどの内部的な動きによって生じる場合もあるし、ハードウェア割り込みシグナルなどの外部の動きによって生じる場合もある。割り込みの実行を完了後に、割り込み前の実行に影響を与えずに継続できる。

この定義はシングルスレッドのプログラミング環境が起源であり、ハードウェア割り込みで割り込まれた制御の流れが割り込みサービスルーチン (ISR) に転送されることから生まれた。ISRが使用するサブルーチンは割り込みをきっかけとして実行される可能性があるため、リエントラントでなければならない。OSのカーネルが使用するサブルーチンの多くは、カーネルで確保済みのリソースを超えられない制限がありリエントラントではない。そのためISRでできることは限られている。例えば、一般にISRからファイルシステムにはアクセスできないし、場合によってはヒープ領域も確保できない。

直接または間接に再帰可能なサブルーチンはリエントラントである。しかし、グローバル変数が処理の流れの中でしか変化しないことを前提としているサブルーチンはリエントラントではない。グローバル変数を更新するサブルーチンが再帰的に呼び出されれば、1回のサブルーチン実行の中でグローバル変数は突然変化することになる。

リエントラント性の概念はシングルスレッドの環境に起源があり、マルチスレッド環境でのスレッドセーフという概念とは異なる。リエントラントなサブルーチンはスレッドセーフにすることもできるが[1]、リエントラントだというだけであらゆる状況でスレッドセーフと言えるわけではない。逆にスレッドセーフなコードはリエントラントである必要はない(後述の例を参照)。

[編集]

次の例の swap() 関数は、リエントラントではない(同時にスレッドセーフでもない)。したがってこれを割り込みサービスルーチン isr() で使用すべきでない。

int t;

void swap(int *x, int *y)
{
  t = *x;
  *x = *y;
  // ここでハード割り込みが起きて isr() が呼び出される可能性がある。
  *y = t;
}

void isr()
{
  int x = 1, y = 2;
  swap(&x, &y);
}

swap()tスレッド局所記憶にすることでスレッドセーフにできる。しかしそのようにしてもリエントラントにはならず、swap() 実行中に同じスレッドのコンテキストで isr() が呼び出されれば問題を生じる可能性が残っている。

次の工夫を加えたswap関数では、実行完了時のグローバルなデータを注意深く一貫性を保つようにしており、完全にリエントラントである。ただし、実行途中のグローバルなデータの一貫性は保証されていないのでスレッドセーフではない。また、int型変数の読み出しおよび書き込みが不可分操作(アトミック)である(読み出し処理および書き込み処理はそれぞれ1命令のみで実行され、処理の最中に割り込みが発生しない)ことが前提である[2][3]

int t;

void swap(int *x, int *y)
{
  int s;

  s = t; // グローバル変数をセーブ
  t = *x;
  *x = *y;
  // ここでハード割り込みが起きて isr() が呼び出される可能性がある。
  *y = t;
  t = s; // グローバル変数をリストア
}

void isr()
{
  int x = 1, y = 2;
  swap(&x, &y);
}

次のswap関数はリエントラントかつスレッドセーフである。

void swap(int *x, int *y)
{
  int t;
  t = *x;
  *x = *y;
  // ここでハード割り込みが起きて isr() が呼び出される可能性がある。
  *y = t;
}

void isr()
{
  int x = 1, y = 2;
  swap(&x, &y);
}

背景

[編集]

リエントラント性と冪等性は同義ではない。冪等な関数は、何度呼び出したとしても1度だけ呼び出したかのように全く同じ出力を生成する。一般化すれば、共有データを使わず、入力データに基づいて出力データを生成する関数である(ただし、入力や出力はオプション)。共有データはいつでも誰でもアクセスできる。データを誰かが更新し、誰も更新を把握していない場合、そのデータが以前と比べて変化したのかどうかさえ誰にもわからない。冪等性はリエントラント性を包含するが、逆は必ずしも真ではない。

データにはスコープという属性がある。グローバルなデータはあらゆる関数のスコープの範囲外にあり、寿命は不定である。一方局所的なデータは関数が呼び出されるたびに生成され、関数から抜けるときに破棄される。

局所的データはルーチン間で共有されず、再入時にも共有されない。したがってリエントラント性を阻害しない。グローバルなデータは任意の関数間で共有でき、あるいは再入時にも共有される。したがってリエントラント性を阻害する。

リエントラント性はスレッドセーフの概念と類似してはいるが、完全に等しいものではない。関数がスレッドセーフであっても、リエントラントでないことがある。例えば関数全体をミューテックスで囲むと、再入が発生しない条件下でのマルチスレッド環境では期待通りの挙動になるが、ミューテックス所有中にスレッドとして実装されていない割り込みが生じて再入が発生するとミューテックスの解放を待ち続けてデッドロックとなる。これはミューテックスが割り込まれた処理と割り込みサービスルーチンの間で共有されていることが原因である。混乱を避ける鍵は、リエントラントが1つのスレッド実行でも問題になるという点である。リエントラント性は割り込まれた処理が割り込みサービスルーチンの終了まで全く動作できないために問題となる性質であり、マルチタスクOSの存在する前からの概念である。

リエントラント性の原則

[編集]
リエントラントなコードは、静的変数やグローバル変数を保持しない。
リエントラントな関数はグローバルなデータを使えないわけではない。例えばリエントラントな割り込みサービスルーチンは、(例えば、シリアルポートのバッファを読み取るなど)ハードウェアのステータス情報を取得できるが、それはグローバルなデータであると同時に揮発性である。それでも静的変数やグローバルなデータを普通に使うことは勧められず、不可分なリード・モディファイ・ライト命令を使ってそのような変数にアクセスするべきである(そのような不可分命令を実行中は割り込みやシグナルが処理を中断できない)。
リエントラントなコードは自分のコードを書き換えない。
OSによってはプロセスが自身のコードを書きかえることを許している。その理由は様々だが(例えば、BitBltでグラフィックスを高速化するためなど)、呼び出す度にコードが変化している可能性があるなら、リエントラント性との両立は難しい。
しかし、呼び出す度にメモリ上の新たな場所にある機械語コードを実行するなら、コードを書き換えても他の呼び出しには影響せず、両立は不可能ではない。
リエントラントなコードは、リエントラントでないプログラムサブルーチンを呼び出さない。
ユーザー/オブジェクト/プロセスに複数レベルの優先度があることや、マルチプロセッシングがリエントラントなコードの制御を複雑化させている。リエントラントな設計においては、あらゆるアクセスに絶えず注意することが重要であり、ルーチン内の副作用に注意することが重要である。

リエントラントな割り込みハンドラ

[編集]

リエントラントな割り込みハンドラは、割り込み処理中に早期に割り込み可能にする割り込みハンドラである。それによって割り込みレイテンシ英語版が低減される[4]。一般に割り込みサービスルーチンをプログラミングする際、割り込みハンドラ内でなるべく早期に割り込み可能な状態にすることが推奨される。それによって割り込みを拾い損なうのを防ぎやすくなる[5]

さらなる例

[編集]

以下のコードにある関数 fg もリエントラントではない

int g_var = 1;

int f()
{
  g_var = g_var + 2;
  return g_var;
}

int g()
{
  return f() + 2;
}

上記のコードで、f はグローバル変数 g_var に依存している。したがって、2つのプロセス(スレッド)が f を実行すると、g_var に同時並行的にアクセスし、結果はタイミングに依存することになる。したがって、f はリエントラントではない。その f を呼び出している g もリエントラントではない。

これらを若干変更したリエントラントである版を以下に示す:

int f(int i)
{
  return i + 2;
}

int g(int i)
{
  return f(i) + 2;
}

新しい版では、グローバル変数 g_var は使われていない。引数を渡して、それに基づいて処理を行って結果を返す。共有される可能性のあるオブジェクトにはアクセスしないようになっている。その代わり、呼出し側が前回の戻り値を引数として渡してやらなければならない。このように、リエントラントなサブルーチンでは、必要な静的データは呼出し側が管理しなければならない。

次のPthreadsを使ったC言語のコードでは、関数functionはスレッドセーフだが、リエントラント(非同期シグナル安全)ではない。Pthreadsのミューテックス関数がリエントラント(非同期シグナル安全)であることは保証されないからである[6]

void function(pthread_mutex_t mutex)
{
  pthread_mutex_lock(mutex);
  /* ... */
  /* 何らかの処理 */
  /* ... */
  pthread_mutex_unlock(mutex);
}

この function は複数のスレッドから呼び出されても全く問題はない。しかし、pthread_mutex_init()によるミューテックスの初期化時にPTHREAD_MUTEX_NORMALを設定した属性を使用していて、リエントラントな割り込みハンドラがこの関数を呼び出す場合、2つ目の割り込みがこの関数実行中に発生すると、二度目の呼び出しはミューテックスを獲得できず、永久に停止(デッドロック)する。割り込みサービスでは他の割り込みをディセーブルするので、システム全体がハングアップすることになる。

デッドロックを回避するには、ミューテックス初期化時に、同一スレッドによる複数回のロックを許可するPTHREAD_MUTEX_RECURSIVEを設定した属性を使用する必要がある[7]。ただし、PTHREAD_MUTEX_RECURSIVEを設定したからといって、pthread_mutex_lock()およびpthread_mutex_unlock()が非同期シグナル安全になるとは限らない。なお、ミューテックス初期化時にPTHREAD_MUTEX_INITIALIZERを使用すると、PTHREAD_MUTEX_DEFAULTを設定した既定の属性が使用されることになり、デッドロックに関しては未定義動作となる。

一方、Microsoft WindowsEnterCriticalSection()関数は、同一スレッドからの複数回呼び出しはブロッキングなしで実行される[8]。ただし非同期シグナル安全性や再入可能性に関する言及や保証はない。

POSIXのリエントラントと非同期シグナル安全

[編集]

POSIX標準には、"_r" の接尾辞が付けられたC言語関数群が用意されている。これらは従来の標準Cライブラリ関数のリエントラントバージョンである。規格では、「リエントラント関数」を以下のように定義している[9]

In POSIX.1c, a "reentrant function" is defined as a "function whose effect, when called by two or more threads, is guaranteed to be as if the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved" (ISO/IEC 9945:1-1996, §2.2.2).
2つ以上のスレッドから呼ばれたときの結果が、たとえ実際の実行がインターリーブ(交互配置)されたものであったとしても、スレッドがそれぞれ未定義の順序でその関数を次々に実行したかのようであることが保証される関数。

つまり、マルチスレッド環境下での実行順序非依存性や並列実行可能性(スレッドセーフ)の意味でリエントラントという用語を規定しており、非同期シグナル安全性に関しては述べられていない。

一方、POSIXでは非同期シグナル安全な118の関数を規定している。シグナルハンドラ内部では非同期シグナル安全でない関数は呼び出してはいけない[10][11]

脚注

[編集]

参考文献

[編集]
  • Kerrisk, Michael (2010). The Linux Programming Interface. No Starch Press 

関連項目

[編集]

外部リンク

[編集]