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

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

直接または間接に再帰可能なサブルーチンは再入可能である。しかし、グローバル変数が処理の流れの中でしか変化しないことを前提としているサブルーチンは再入可能ではない。グローバル変数を更新するサブルーチンが再帰的に呼び出されれば、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命令のみで実行され、処理の最中に割り込みが発生しない)ことが前提であるUse reentrant functions for safer signal handling | IBM Developer安全なシグナル処理のために再入可能ファンクションを使う | IBM Developer。

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でグラフィックスを高速化するためなど)、呼び出す度にコードが変化している可能性があるなら、再入可能性との両立は難しい。 しかし、呼び出す度にメモリ上の新たな場所にある機械語コードを実行するなら、コードを書き換えても他の呼び出しには影響せず、両立は不可能ではない。 再入可能なコードは、再入可能でないプログラムやサブルーチンを呼び出さない。 ユーザー/オブジェクト/プロセスに複数レベルの優先度があることや、マルチプロセッシングが再入可能なコードの制御を複雑化させている。再入可能な設計においては、あらゆるアクセスに絶えず注意することが重要であり、ルーチン内の副作用に注意することが重要である。

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

さらなる例 以下のコードにある関数 f も g も再入可能ではない。

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のミューテックス関数が再入可能(非同期シグナル安全)であることは保証されないからであるMan page of PTHREAD_MUTEX。

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を設定した属性を使用する必要があるpthread_mutexattr_settype(3) - Linux man page。ただし、PTHREAD_MUTEX_RECURSIVEを設定したからといって、pthread_mutex_lock()およびpthread_mutex_unlock()が非同期シグナル安全になるとは限らない。なお、ミューテックス初期化時にPTHREAD_MUTEX_INITIALIZERを使用すると、PTHREAD_MUTEX_DEFAULTを設定した既定の属性が使用されることになり、デッドロックに関しては未定義動作となる。

一方、Microsoft WindowsのEnterCriticalSection()関数は、同一スレッドからの複数回呼び出しはブロッキングなしで実行されるEnterCriticalSection function | Microsoft Docs After a thread has ownership of a critical section, it can make additional calls to EnterCriticalSection or TryEnterCriticalSection without blocking its execution. This prevents a thread from deadlocking itself while waiting for a critical section that it already owns.。ただし非同期シグナル安全性や再入可能性に関する言及や保証はない。

POSIXの再入可能と非同期シグナル安全 POSIX標準には、"_r" の接尾辞が付けられたC言語関数群が用意されている。これらは従来の標準Cライブラリ関数の再入可能バージョンである。規格では、「再入可能 (reentrant) 関数」を以下のように定義しているThread-safety and POSIX.1。

2つ以上のスレッドから呼ばれたときの結果が、たとえ実際の実行がインターリーブ(交互配置)されたものであったとしても、スレッドがそれぞれ未定義の順序でその関数を次々に実行したかのようであることが保証される関数。

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

一方、POSIXでは非同期シグナル安全な118の関数を規定している。シグナルハンドラ内部では非同期シグナル安全でない関数は呼び出してはいけないSignal Handlers and Async-Signal Safety (Multithreaded Programming Guide) | Oracleシグナルハンドラと「非同期シグナル安全」 (マルチスレッドのプログラミング) | Oracle。

脚注

参考文献

関連項目 参照透過性 スレッドセーフ 再帰

外部リンク

"再入可能コードおよびスレッド・セーフ・コードの作成," from AIX Version 6.1 プログラミングの一般概念: プログラムの作成およびデバッグ Jack Ganssle (2001). "Introduction to Reentrancy". EE Times. Raymond Chen (2004). The difference between thread-safety and re-entrancy. The Old New Thing.

リエントラント 【 reentrant 】 再入可能 - IT用語事典e-words