最近、「mallocスレッドは安全ですか?」というタイトルで質問しました。、そしてその中で私は「mallocはリエントラントですか?」と尋ねました。
私は、すべての再入可能性がスレッドセーフであるという印象を受けました。
この仮定は間違っていますか?
最近、「mallocスレッドは安全ですか?」というタイトルで質問しました。、そしてその中で私は「mallocはリエントラントですか?」と尋ねました。
私は、すべての再入可能性がスレッドセーフであるという印象を受けました。
この仮定は間違っていますか?
回答:
再入可能関数は、Cライブラリヘッダーで公開されているグローバル変数に依存しません。たとえば、Cではstrtok()とstrtok_r()を使用します。
一部の関数には「進行中の作業」を格納する場所が必要です。再入可能関数を使用すると、グローバルではなく、スレッド自体のストレージ内でこのポインターを指定できます。このストレージは呼び出し元の関数専用であるため、中断して再入力(再入可能)することができ、ほとんどの場合、関数が実装するものを超える相互排除はこれが機能するために必要ではないため、次のように見なされます。スレッドセーフ。ただし、これは定義上保証されていません。
ただし、errnoはPOSIXシステムではわずかに異なるケースです(そして、これがどのように機能するかを説明する上で奇妙な傾向があります):)
要するに、リエントラントはしばしばスレッドセーフを意味しますが(「スレッドを使用している場合はその関数のリエントラントバージョンを使用する」のように)、スレッドセーフは必ずしもリエントラント(またはその逆)を意味するわけではありません。スレッドセーフを検討している場合、並行性について考える必要があります。関数を使用するためにロックと相互排除の手段を提供する必要がある場合、その関数は本質的にスレッドセーフではありません。
ただし、すべての機能を調べる必要はありません。malloc()
再入可能である必要はありません。特定のスレッドのエントリポイントの範囲外のものに依存しません(そしてそれ自体がスレッドセーフです)。
静的に割り当てられた値を返す関数は、ミューテックス、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;
}
したがって、ご覧のとおり、複数のスレッドでそれを使用することは、なんらかのロックなしで災害になります。しかし、再入可能にする目的はありません。一部の組み込みプラットフォームで動的に割り当てられたメモリがタブーである場合、これに遭遇します。
純粋な関数型プログラミングでは、リエントラントはスレッドセーフを意味しないことが多く、関数のエントリポイントに渡される定義済み関数または無名関数の動作、再帰などに依存します。
「スレッドセーフ」を配置するためのより良い方法は、同時アクセスに対して安全であり、これは必要性をよりよく示しています。
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;
}
t = *x
に割り込み、を呼び出すとswap()
、t
オーバーライドされ、予期しない結果が発生します。
swap(5, 6)
によって中断された呼び出しについて考えてみましょうswap(1, 2)
。の後t=*x
、s=t_original
およびt=5
。さて、中断後、s=5
そしてt=1
。ただし、2番目swap
が戻る前に、コンテキストが復元され、が作成されt=s=5
ます。今、私たちは、最初に戻っswap
てt=5 and s=t_original
以降継続しますt=*x
。したがって、関数は再入可能であるように見えます。すべての呼び出しは、s
スタックに割り当てられた独自のコピーを取得することに注意してください。
共有データへのすべての参照がシリアル化されるため、呼び出しで共有データが使用されている場合でも、スレッドセーフ*関数を複数のスレッドから同時に呼び出すことができます。
再入可能関数は、複数のスレッドから同時に呼び出すことができますが、それぞれの呼び出しは、独自のデータを使用する場合にのみ。
したがって、スレッドセーフ関数は常に再入可能ですが、再入可能関数は常にスレッドセーフであるとは限りません。
ひいては、各スレッドがクラスの異なるインスタンスを使用している限り、そのメンバー関数を複数のスレッドから安全に呼び出すことができる場合、クラスは再入可能であると言われます。すべてのスレッドがクラスの同じインスタンスを使用している場合でも、そのメンバー関数を複数のスレッドから安全に呼び出すことができれば、クラスはスレッドセーフです。
しかし、彼らはまた警告します:
注:マルチスレッドドメインの用語は完全に標準化されているわけではありません。POSIXは、CAPIでは多少異なるリエントラントとスレッドセーフの定義を使用します。Qtで他のオブジェクト指向C ++クラスライブラリを使用する場合は、定義が理解されていることを確認してください。