ほとんどofthetimes 、再入の定義は Wikipedia から引用されています:
コンピュータープログラムまたはルーチンは、safelyを前の呼び出しの前に再度呼び出すことができる場合、再入可能として記述されます完了しました(つまり、同時に安全に実行できます)。再入可能にするには、コンピュータープログラムまたはルーチン:
- 静的(またはグローバル)非定数データを保持してはなりません。
- 静的(またはグローバル)非定数データにアドレスを返してはなりません。
- 呼び出し元から提供されたデータでのみ機能する必要があります。
- シングルトンリソースへのロックに依存してはなりません。
- 独自のコードを変更しないでください(独自のスレッドストレージで実行しない限り)
- 再入不可のコンピュータープログラムまたはルーチンを呼び出さないでください。
安全に定義する方法は?
プログラムを安全に同時に実行できる場合、それは常に再入可能という意味ですか?
上記の6つのポイント間の共通のスレッドとは、コードのリエントラント機能をチェックする際に留意すべきことです。
また、
この質問を書いているときに、1つ気が付きます:reentranceやthread safe絶対的です。つまり、具体的な定義が修正されていますか?なぜなら、そうでない場合、この質問はあまり意味がありません。
意味的に。この場合、これは明確に定義された用語ではありません。それは「リスクなしでそれを行うことができる」という意味です。
番号。
たとえば、ロックとコールバックの両方をパラメーターとして受け取るC++関数を考えてみましょう。
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
別の関数は、同じミューテックスをロックする必要があります。
void bar()
{
foo(nullptr);
}
一見、すべてが大丈夫のように見えます...
int main()
{
foo(bar);
return 0;
}
Mutexのロックが再帰的でない場合、メインスレッドで次のようになります。
main
はfoo
を呼び出します。foo
がロックを取得します。foo
はbar
を呼び出し、これはfoo
を呼び出します。foo
はロックの取得を試み、失敗し、ロックが解放されるのを待ちます。わかりました、私はコールバックの事を使用してごまかしました。しかし、同様の効果を持つより複雑なコードを想像するのは簡単です。
あなたはできる におい 関数が変更可能な永続リソースへのアクセスを持っている/与える、または次の関数へのアクセスを持っている/与える場合の問題 においがする。
(OK、私たちのコードの99%は匂いがするはずです...それを処理するための最後のセクションを参照してください...)
そのため、コードを調べると、これらのポイントの1つが警告するはずです。
非再入可能性はウイルス性であることに注意してください。可能な再入不可関数を呼び出すことができる関数は、再入可能とは見なされません。
C++メソッドにも注意してください におい this
にアクセスできるため、コードを調べて、面白い相互作用がないことを確認する必要があります。
番号。
マルチスレッドの場合、共有リソースにアクセスする再帰関数が複数のスレッドから同時に呼び出され、データが破損または破損する可能性があります。
シングルスレッドの場合、再帰関数は、非リエントラント関数(悪名高いstrtok
など)を使用するか、データが既に使用されているという事実を処理せずにグローバルデータを使用できます。したがって、関数はそれ自体を直接または間接的に呼び出すため再帰的ですが、それでも 再帰的ではない。
上記の例では、明らかにスレッドセーフな関数がリエントラントではないことを示しました。 Okコールバックパラメーターのためにだまされました。しかし、その後、スレッドに非再帰的ロックを2回取得させることにより、スレッドをデッドロックする方法が複数あります。
「再帰的」とは「再帰的安全」を意味する場合、「はい」と言います。
関数が複数のスレッドによって同時に呼び出されることを保証でき、問題なく直接または間接的にそれ自体を呼び出すことができる場合、再入可能です。
問題はこの保証を評価している…^ _ ^
私は彼らが持っていると信じていますが、その後、関数がスレッドセーフか再入可能かを評価することは困難です。これが私が用語を使用した理由です におい 上記:関数はリエントラントではないことがわかりますが、複雑なコードがリエントラントであることを確認するのは難しい場合があります
リソースを使用する必要のある1つのメソッドを持つオブジェクトがあるとします。
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
最初の問題は、この関数が何らかの形で再帰的に呼び出されると(つまり、この関数が直接または間接的にそれ自体を呼び出す)、this->p
が最後の呼び出しの終わりに削除され、おそらく最初の呼び出しの終了前に使用されます。
したがって、このコードは 再帰安全。
これを修正するには、参照カウンターを使用できます。
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
この方法で、コードは再帰的に安全になります...しかし、マルチスレッドの問題のため、リエントラントではありません:c
およびp
の変更は、 再帰的 ミューテックス(すべてのミューテックスが再帰的ではありません):
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
そしてもちろん、これはすべてp
の使用を含め、lots of code
自体がリエントラントであることを前提としています。
また、上記のコードはリモートでもありません exception-safe ですが、これは別の話です…^ _ ^
スパゲッティコードについては非常に真実です。ただし、コードを正しくパーティション分割すれば、再入問題を回避できます。
パラメータ、独自のローカル変数、状態のない他の関数のみを使用し、データがまったく返された場合はデータのコピーを返す必要があります。
オブジェクトメソッドはthis
にアクセスできるため、オブジェクトの同じインスタンスのすべてのメソッドと状態を共有します。
そのため、オブジェクトがスタック内のあるポイントで(つまり、メソッドAを呼び出す)、次に別のポイント(つまり、メソッドBを呼び出す)で、オブジェクト全体を破損することなく使用できることを確認してください。オブジェクトを設計して、メソッドの終了時にオブジェクトが安定して正しいことを確認します(ぶら下がりポインター、矛盾するメンバー変数などはありません)。
他の誰も内部データにアクセスできません。
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
使用がデータのアドレスを取得する場合、const参照を保持するコードが通知されることなくコードの他の部分がそれを変更する可能性があるため、const参照を返すことは危険です。
したがって、ユーザーは、ミューテックスを使用してスレッド間で共有されるオブジェクトを使用する責任があります。
STLのオブジェクトは(パフォーマンスの問題のため)スレッドセーフではないように設計されているため、ユーザーがstd::string
を2つのスレッド間で共有する場合、ユーザーは同時アクセスプリミティブでアクセスを保護する必要があります。
これは、同じスレッドが同じリソースを2回使用できると思われる場合、再帰的なmutexを使用することを意味します。
「安全に」とは、常識が指示するとおりに定義されます-それは、「他のことを妨げることなく、そのことを正しく行う」ことを意味します。あなたが引用する6つのポイントは、それを達成するための要件を非常に明確に表しています。
あなたの3つの質問に対する答えは3×「いいえ」です。
すべての再帰関数はリエントラントですか?
NO!
再帰関数の2つの同時呼び出しは、たとえば同じグローバル/静的データにアクセスする場合、簡単にお互いを台無しにする可能性があります。
すべてのスレッドセーフ関数は再入可能ですか?
NO!
関数は、同時に呼び出されても誤動作しない場合、スレッドセーフです。しかし、これは実現できます。ミューテックスを使用して、最初の呼び出しが完了するまで2番目の呼び出しの実行をブロックするため、一度に1つの呼び出しのみが機能します。再入可能とは、他の呼び出しを妨げることなく同時に実行することを意味します。
すべての再帰関数およびスレッドセーフ関数は再入可能ですか?
NO!
上記を参照。
共通のスレッド:
ルーチンが中断されている間に呼び出された場合の動作は明確に定義されていますか?
このような関数がある場合:
int add( int a , int b ) {
return a + b;
}
その場合、外部状態に依存しません。動作は明確に定義されています。
このような関数がある場合:
int add_to_global( int a ) {
return gValue += a;
}
結果は、複数のスレッドで適切に定義されていません。タイミングが間違っていた場合、情報が失われる可能性があります。
再入可能な関数の最も単純な形式は、渡された引数と定数値でのみ動作するものです。それ以外のものは特別な処理を必要とするか、多くの場合、リエントラントではありません。そしてもちろん、引数は可変グローバルを参照してはなりません。
次に、以前のコメントについて詳しく説明する必要があります。 @paercebalの答えは間違っています。例のコードでは、パラメータとなるはずのミューテックスが実際に渡されなかったことに誰も気づきませんでしたか?
私は結論に異議を唱え、私は主張する:並行性の存在下で機能が安全であるためには、再入可能でなければならない。したがって、コンカレントセーフ(通常はスレッドセーフで記述)は、リエントラントを意味します。
スレッドセーフでもリエントラントでも、引数については何も言うことはありません。関数の同時実行について話しているので、不適切なパラメーターが使用された場合でも安全ではありません。
たとえば、memcpy()はスレッドセーフで再入可能です(通常)。 2つの異なるスレッドから同じターゲットへのポインターで呼び出された場合、明らかに期待どおりに機能しません。これがSGI定義のポイントであり、同じデータ構造へのアクセスがクライアントによって同期されるように、クライアントに責任を負わせます。
一般に、スレッドセーフ操作にパラメータを含めることはナンセンスであることを理解することが重要です。データベースプログラミングを行ったことがあれば理解できます。 「アトミック」であり、ミューテックスまたはその他の手法によって保護される可能性があるという概念は、ユーザー概念です。同期を維持する必要があるのはクライアントプログラマーですが、誰が言うことができますか?
重要なのは、「破損」がシリアル化されていない書き込みでコンピューターのメモリを台無しにする必要がないということです。個々の操作がすべてシリアル化された場合でも破損が発生する可能性があります。したがって、関数がスレッドセーフか再入可能かを尋ねるとき、質問は適切に分離されたすべての引数を意味します:結合引数の使用は反例を構成しません。
プログラミングシステムは数多くあります。Ocamlは1つであり、Pythonも同様であると思います。これには多くの非リエントラントコードがありますが、グローバルロックを使用してスレッドアクセスをインターリーブします。これらのシステムはリエントラントではなく、スレッドセーフまたはコンカレントセーフではありません。グローバルに同時実行を防ぐため、安全に動作します。
良い例はmallocです。再入可能ではなく、スレッドセーフでもありません。これは、グローバルリソース(ヒープ)にアクセスする必要があるためです。ロックを使用しても安全ではありません。絶対に再入可能ではありません。 mallocへのインターフェイスが適切に設計されていた場合、再入可能かつスレッドセーフにすることができます。
malloc(heap*, size_t);
これで、共有アクセスを1つのヒープにシリアル化する責任をクライアントに移すため、安全になります。特に、個別のヒープオブジェクトがある場合、作業は必要ありません。共通ヒープが使用される場合、クライアントはアクセスをシリアル化する必要があります。ロックの使用inside関数は十分ではありません:ヒープ*をロックするmallocを考慮すると、同じポインタでシグナルが来てmallocを呼び出します:デッドロック:シグナルは続行できません。クライアントは中断されるため、どちらもできません。
一般的に言えば、ロックは物事をスレッドセーフにしない..実際には、クライアントが所有するリソースを不適切に管理しようとすることにより、安全性を破壊します。ロックは、オブジェクトの製造元が行う必要があります。これは、作成されるオブジェクトの数と使用方法を知る唯一のコードです。
リストされているポイントの中で「共通スレッド」(しゃれが意図されています!?)は、関数が同じ関数の再帰呼び出しまたは同時呼び出しの動作に影響を与えるようなことをしてはいけないことです.
たとえば、静的データはすべてのスレッドによって所有されているため、問題です。 1つの呼び出しで静的変数が変更されると、すべてのスレッドが変更されたデータを使用するため、それらの動作に影響します。複数のスレッドがありますが、コードのコピーは1つしかないため、コードの自己変更(めったに発生せず、場合によっては防止されます)が問題になります。コードも重要な静的データです。
本質的に再入可能であるためには、各スレッドは、それが唯一のユーザーであるかのように関数を使用できなければなりません。これには主に、関数が機能する個別のデータまたは定数データのある各スレッドが含まれます。
つまり、ポイント(1)は必ずしも真実ではありません。たとえば、正当かつ設計上、静的変数を使用して再帰カウントを保持し、過度の再帰を防止したり、アルゴリズムをプロファイリングしたりできます。
スレッドセーフな関数は再入可能である必要はありません。ロックによる再入を明確に防止することでスレッドの安全性を達成できます。また、ポイント(6)は、そのような関数は再入不可能であると述べています。ポイント(6)に関して、ロックするスレッドセーフ関数を呼び出す関数は、再帰での使用に対して安全ではない(デッドロックします)ため、再入可能とは言われませんが、並行性に対して安全である場合があります。複数のスレッドがそのような関数のプログラムカウンターを同時に持つことができるという意味で、リエントラントのままです(ロックされた領域ではない)。これは、スレッド安全性と再入可能性を区別するのに役立つ場合があります(または混乱を招く可能性があります!)。
あなたの「また」質問に対する答えは、「いいえ」、「いいえ」、「いいえ」です。関数が再帰的および/またはスレッドセーフであるからといって、再入可能になりません。
これらのタイプの関数はそれぞれ、引用するすべてのポイントで失敗する可能性があります。 (私はポイント5の100%確実ではありませんが)。
「スレッドセーフ」および「再入可能」という用語は、それらの定義が意味するものだけを意味します。このコンテキストでの「安全」とは、onlyの下に引用する定義の意味を意味します。
ここで言う「安全」とは、特定のコンテキストで特定の関数を呼び出してもアプリケーションが完全に機能しないという意味で、安全という意味ではありません。全体として、関数はマルチスレッドアプリケーションで目的の効果を確実に生成する可能性がありますが、定義に従ってリエントラントまたはスレッドセーフとして認定されません。逆に、マルチスレッドアプリケーションでさまざまな望ましくない、予期しない、および/または予測できない効果を生成する方法で、リエントラント関数を呼び出すことができます。
再帰関数は何でもかまいませんし、リエントラントはスレッドセーフよりも強力な定義を持っているので、番号付きの質問に対する答えはすべてノーです。
リエントラントの定義を読んで、それを修正するために呼び出したものを超えて何も変更しない関数を意味するものとして要約するかもしれません。しかし、要約だけに頼るべきではありません。
マルチスレッドプログラミングは、一般的なケースでは 非常に難しい です。コードのリエントラントのどの部分を知ることは、この課題の一部にすぎません。スレッドセーフは付加的ではありません。リエントラント関数をつなぎ合わせるのではなく、全体的な thread-safedesign pattern を使用し、このパターンを使用してeveryスレッドおよびプログラム内の共有リソース。