最近、 「mallocスレッドは安全ですか?」というタイトルで質問しました。、そしてその中で私は「mallocはリエントラントですか?」と尋ねました。
私は、すべての再入可能性がスレッドセーフであるという印象を受けました。
この仮定は間違っていますか?
最近、 「mallocスレッドは安全ですか?」というタイトルで質問しました。、そしてその中で私は「mallocはリエントラントですか?」と尋ねました。
私は、すべての再入可能性がスレッドセーフであるという印象を受けました。
この仮定は間違っていますか?
TL;DR: 関数は、再入可能、スレッドセーフ、またはその両方である場合と、どちらでもない場合があります。
スレッドセーフと再入可能性に関するウィキペディアの記事は、読む価値があります。ここにいくつかの引用があります:
次の場合、関数はスレッドセーフです。
同時に複数のスレッドによる安全な実行を保証する方法で共有データ構造を操作するだけです。
次の場合、関数は再入可能です。
実行中の任意の時点で中断することができ、その後、以前の呼び出しの実行が完了する前に、安全に再度呼び出す (「再入力」) ことができます。
再入可能性の例として、ウィキペディアは、システム割り込みによって呼び出されるように設計された関数の例を示しています。別の割り込みが発生したときに、関数が既に実行されているとします。ただし、システム割り込みを使用してコーディングしていないからといって安全だとは思わないでください。コールバックまたは再帰関数を使用すると、シングルスレッド プログラムで再入の問題が発生する可能性があります。
混乱を避けるための鍵は、再入可能とは実行中の 1 つのスレッドのみを指すということです。これは、マルチタスクのオペレーティング システムが存在しなかった時代からの概念です。
例
(ウィキペディアの記事を一部改変)
例 1: スレッドセーフではなく、再入可能ではありません
/* As this function uses a non-const global variable without
any precaution, it is neither reentrant nor thread-safe. */
int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
*y = t;
}
例 2: スレッドセーフ、再入不可
/* We use a thread local variable: the function is now
thread-safe but still not reentrant (within the
same thread). */
__thread int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
*y = t;
}
例 3: スレッドセーフではなく、再入可能
/* We save the global state in a local variable and we restore
it at the end of the function. The function is now reentrant
but it is not thread safe. */
int t;
void swap(int *x, int *y)
{
int s;
s = t;
t = *x;
*x = *y;
*y = t;
t = s;
}
例 4: スレッドセーフ、再入可能
/* We use a local variable: the function is now
thread-safe and reentrant, we have ascended to
higher plane of existence. */
void swap(int *x, int *y)
{
int t;
t = *x;
*x = *y;
*y = t;
}
定義によります。たとえば、Qt は以下を使用します。
共有データへのすべての参照がシリアル化されるため、呼び出しで共有データが使用される場合でも、スレッドセーフ* 関数は複数のスレッドから同時に呼び出すことができます。
再入可能関数は複数のスレッドから同時に呼び出すこともできますが、それは各呼び出しが独自のデータを使用する場合のみです。
したがって、スレッド セーフ関数は常に再入可能ですが、再入可能関数は常にスレッド セーフとは限りません。
拡張により、各スレッドがクラスの異なるインスタンスを使用する限り、メンバー関数を複数のスレッドから安全に呼び出すことができる場合、そのクラスは再入可能であると言われます。すべてのスレッドがクラスの同じインスタンスを使用している場合でも、そのメンバー関数を複数のスレッドから安全に呼び出すことができる場合、そのクラスはスレッドセーフです。
しかし、彼らは次のようにも警告しています。
注:マルチスレッド ドメインの用語は、完全には標準化されていません。POSIX では、C API とは多少異なる再入可能およびスレッドセーフの定義を使用しています。Qt で他のオブジェクト指向の C++ クラス ライブラリを使用する場合は、定義を理解していることを確認してください。
再入可能関数は、C ライブラリ ヘッダーで公開されているグローバル変数に依存しません。たとえば、C では strtok() と strtok_r() を比較します。
一部の関数では、'進行中の作業' を格納する場所が必要です。再入可能関数を使用すると、グローバルではなく、スレッド自体のストレージ内でこのポインターを指定できます。このストレージは呼び出し元の関数専用であるため、中断して再入することができます(再入可能)。ほとんどの場合、これが機能するために関数が実装する以上の相互排除は必要ないため、多くの場合、それらは機能すると見なされます。スレッドセーフ。ただし、これは定義上保証されていません。
ただし、errno は POSIX システムでは少し異なるケースです (そして、これがどのように機能するかについての説明では、奇妙なものになる傾向があります) :)
要するに、再入可能はしばしばスレッドセーフを意味します (「スレッドを使用している場合は、その関数の再入可能バージョンを使用する」など) が、スレッドセーフは常に再入可能 (またはその逆) を意味するとは限りません。スレッド セーフを検討する場合、並行性について考慮する必要があります。関数を使用するためにロックと相互排除の手段を提供する必要がある場合、その関数は本質的にスレッドセーフではありません。
ただし、すべての関数を調べる必要があるわけではありません。malloc()
再入可能である必要はなく、特定のスレッドのエントリ ポイントの範囲外のものには依存しません (それ自体がスレッド セーフです)。
静的に割り当てられた値を返す関数は、mutex、futex、またはその他のアトミック ロック メカニズムを使用しない限り、スレッド セーフではありません。ただし、中断されることがなければ、再入可能である必要はありません。
すなわち:
static char *foo(unsigned int flags)
{
static char ret[2] = { 0 };
if (flags & FOO_BAR)
ret[0] = 'c';
else if (flags & BAR_FOO)
ret[0] = 'd';
else
ret[0] = 'e';
ret[1] = 'A';
return ret;
}
したがって、ご覧のとおり、複数のスレッドが何らかのロックなしでそれを使用することは災害になります..しかし、再入可能にする目的はありません。一部の組み込みプラットフォームで動的に割り当てられたメモリがタブーである場合、それに遭遇します。
純粋な関数型プログラミングでは、再入可能はスレッドセーフを意味しないことが多く、関数のエントリポイント、再帰などに渡される定義済み関数または無名関数の動作に依存します。
「スレッド セーフ」を配置するより良い方法は、同時アクセスに対して安全であり、その必要性をよりよく示しています。