このC#メソッドがスレッドセーフかどうかを確認するにはどうすればよいですか?
-
05-07-2019 - |
質問
ASP.NETキャッシュアイテム削除イベントのコールバック関数の作成に取り組んでいます。
ドキュメントでは、静的メソッドなど、オブジェクトのメソッドまたは存在することがわかっている呼び出し(スコープ内にある)を呼び出す必要があると書かれていますが、静的がスレッドセーフであることを確認する必要があります。
パート1:非スレッドセーフにするためにできることの例は何ですか?
パート2:これは、私が持っている場合
static int addOne(int someNumber){
int foo = someNumber;
return foo +1;
}
そしてClass.addOne(5)を呼び出します;およびClass.addOne(6);同時に、どの呼び出しがfooを最初に設定するかによって6または7が返される場合がありますか? (つまり、競合状態)
解決
addOne 関数は、別のスレッドがアクセスできるデータにはアクセスしないため、実際にスレッドセーフです。各スレッドは独自のスタックを取得するため、ローカル変数はスレッド間で共有できません。ただし、関数のパラメーターが参照型ではなく値型であることを確認する必要があります。
static void MyFunction(int x) { ... } // thread safe. The int is copied onto the local stack.
static void MyFunction(Object o) { ... } // Not thread safe. Since o is a reference type, it might be shared among multiple threads.
他のヒント
いいえ、addOneはここではスレッドセーフです-ローカル変数のみを使用します。次に、スレッドセーフではない 例を示します。
class BadCounter
{
private static int counter;
public static int Increment()
{
int temp = counter;
temp++;
counter = temp;
return counter;
}
}
ここでは、2つのスレッドの両方が同時にIncrementを呼び出し、最終的に1回しかインクリメントできません。
(ちなみに、return ++counter;
を使用するのも同じように悪いことです。上記は同じことのより明示的なバージョンです。私はそれを拡張してより明らかに間違っています。)
スレッドセーフであるかどうかの詳細は非常に難しい場合がありますが、一般に、状態を変更しない場合(とにかく、渡されたもの以外-そこにある灰色の領域) 通常大丈夫です。
スレッド化の問題(最近私も心配していました)は、個別のキャッシュを持つ複数のプロセッサコアの使用、および基本的なスレッド交換の競合状態から発生します。別々のコアのキャッシュが同じメモリの場所にアクセスする場合、通常、他のコアのキャッシュがわからないため、メインのメモリに戻らずに(またはすべてで共有された同期キャッシュにせずに、そのデータの場所の状態を個別に追跡できます)プロセッサのパフォーマンス上の理由から、L2またはL3のコアなど)。したがって、マルチスレッド環境では、実行順序のインターロックトリックでさえ信頼できない場合があります。
ご存知かもしれませんが、これを修正する主なツールはロックです。ロックは排他的アクセスのメカニズムを提供し(同じロックの競合間)、基礎となるキャッシュ同期を処理し、さまざまな方法で同じメモリ位置にアクセスしますロック保護されたコードセクションは適切にシリアル化されます。誰がいつどのような順序でロックを取得するかの間で競合状態が発生する可能性がありますが、通常、ロックされたセクションの実行がアトミックであることを保証できる場合(そのロックのコンテキスト内で)対処する方がはるかに簡単です。
任意の参照型のインスタンスのロックを取得できます(たとえば、intやenumなどの値型ではなく、null以外のObjectから継承します)が、オブジェクトのロックには固有のものがないことを理解することが非常に重要ですそのオブジェクトへのアクセスに影響し、同じオブジェクトのロックを取得する他の試みとのみ相互作用します。適切なロックスキームを使用してメンバー変数へのアクセスを保護するのは、クラス次第です。インスタンスは、自分自身をロックすることで自分のメンバーへのマルチスレッドアクセスを保護する場合があります(例:lock (this) { ... }
)が、インスタンスは1人の所有者のみが保持する傾向があり、スレッドセーフなアクセスを保証する必要がないため、通常は必要ありませんインスタンス。
より一般的には、クラスはプライベートロックを作成します(たとえば、各インスタンス内の個別のロックの場合はprivate readonly object m_Lock = new Object();
、そのインスタンスのメンバーへのアクセスを保護する場合はprivate static readonly object s_Lock = new Object();
、集中ロックの場合はクラスの静的メンバーへのアクセスを保護します) 。 Joshには、ロックを使用するより具体的なコード例があります。次に、ロックを適切に使用するようにクラスをコーディングする必要があります。より複雑な場合は、異なるグループのメンバーに対して個別のロックを作成して、一緒に使用されないさまざまな種類のリソースの競合を減らすこともできます。
したがって、元の質問に戻すために、独自のローカル変数とパラメータにのみアクセスするメソッドはスレッドセーフになります。これらは現在のスレッドに固有のスタック上の独自のメモリ位置に存在し、渡される前にスレッド間でこれらのパラメータインスタンスを共有していない限り、他の場所にはアクセスできません。
インスタンスのメンバー(静的メンバーではない)にのみアクセスする非静的メソッド-そしてもちろんパラメーターとローカル変数-は、単一の所有者が使用するインスタンスのコンテキストでロックを使用する必要はありません(スレッドセーフである必要はありません)が、インスタンスを共有することを意図しており、スレッドセーフアクセスを保証したい場合、インスタンスはそのインスタンスに固有の1つ以上のロックでそのメンバー変数へのアクセスを保護する必要があります(インスタンス自体のロックは1つのオプションです)-スレッドセーフな共有を目的としないものを共有する場合、呼び出し側にそれ自身のロックを実装するように任せるのではなく、
操作されていない読み取り専用メンバー(静的または非静的)へのアクセスは一般に安全ですが、保持するインスタンス自体がスレッドセーフでない場合、または複数の操作にわたってアトミック性を保証する必要がある場合は、独自のロックスキームを使用して、すべてのアクセスを保護する必要がある場合もあります。これは、インスタンスが複数のインスタンスでロックを取得できるため、インスタンスがそれ自体でロックを使用する場合に便利な場合です原子性のためにアクセスしますが、それ自体のロックを使用してそれらのアクセスを個別にスレッドセーフにする場合は、単一アクセスの場合はそうする必要はありません。 (クラスでない場合は、それ自体でロックするか、外部からアクセスできないプライベートロックを使用しているかどうかを知る必要があります。)
そして最後に、インスタンス内から(特定のメソッドまたは他のメソッドによって変更される)静的メンバーの変更へのアクセスがあります-そしてもちろん、静的メンバーにアクセスし、誰からでも、どこからでも、いつでも呼び出すことができる静的メソッド-責任あるロックを使用する必要性が最も高く、これがないと、スレッドセーフではなく、予測できないバグを引き起こす可能性があります。
.NETフレームワーククラスを扱う場合、Microsoftは、特定のAPI呼び出しがスレッドセーフであるかどうかをMSDNで文書化します(たとえば、List<T>
などの提供されたジェネリックコレクションタイプの静的メソッドはスレッドセーフになりますが、インスタンスメソッドは-しかし、特に確認してください)。ほとんどの場合(そして、特にスレッドセーフであると言わない限り)、内部的にスレッドセーフではないため、安全に使用するのはユーザーの責任です。また、個々の操作が内部的にスレッドセーフで実装されている場合でも、アトミックである必要があるより複雑な処理を行う場合、コードによるアクセスの共有と重複について心配する必要があります。
1つの大きな注意点は、コレクションを反復処理することです(例:foreach
を使用)。コレクションへの各アクセスが安定した状態になったとしても、それらのアクセスの間に変更が行われないという固有の保証はありません(他の場所にアクセスできる場合)。コレクションがローカルに保持されている場合、通常は問題ありませんが、(別のスレッドまたはループの実行中に)変更できるコレクションは一貫性のない結果を生成する可能性があります。これを解決する簡単な方法の1つは、アトミックスレッドセーフ操作(保護ロックスキーム内)を使用してコレクションの一時コピー(MyType[] mySnapshot = myCollection.ToArray();
)を作成し、ロック外のローカルスナップショットコピーを反復処理することです。多くの場合、これにより、ロックをずっと保持する必要がなくなりますが、反復内で何をしているのかによっては、これで十分ではなく、常に変更から保護する必要があります(または、すでに内部にあるかもしれません)コレクションを他のものと一緒に変更するためのアクセスを防ぐロックされたセクションです。
そのため、スレッドセーフな設計には少々芸術があり、物を保護するためにロックを取得する場所と方法を知ることは、クラスの全体的な設計と使用法に大きく依存します。偏執狂になりやすく、あらゆるものをロックする必要があると考えることは簡単ですが、実際には、物事を保護する適切な層を見つけることが重要です。
ローカル変数のみを使用しているため、メソッドは問題ありません。メソッドを少し変更してみましょう。
static int foo;
static int addOne(int someNumber)
{
foo=someNumber;
return foo++;
}
これは静的データに触れているため、スレッドセーフな方法ではありません。これは、次のように変更する必要があります。
static int foo;
static object addOneLocker=new object();
static int addOne(int someNumber)
{
int myCalc;
lock(addOneLocker)
{
foo=someNumber;
myCalc= foo++;
}
return myCalc;
}
これはばかげたサンプルだと思うのですが、正しく読んでいる場合にfooにはもう意味はありませんが、サンプルです。
非スレッドセーフコードを検出できるようにする研究が進行中です。例えば。プロジェクト Microsoft ResearchのCHESS 。
これは、関数の外部の変数を変更している場合にのみ競合状態になります。あなたの例はそれをしていません。
それは基本的にあなたが探しているものです。スレッドセーフとは、関数が次のいずれかであることを意味します。
- 外部データを変更しない、または
- 外部データへのアクセスは適切に同期されるため、一度に1つの機能のみがアクセスできます。
外部データは、ストレージに保持されているもの(データベース/ファイル)、またはアプリケーションの内部にあるもの(変数、クラスのインスタンスなど)です。基本的には、世界のどこでも、関数のスコープ。
関数の非スレッドセーフバージョンの簡単な例は次のとおりです。
private int myVar = 0;
private void addOne(int someNumber)
{
myVar += someNumber;
}
同期せずに2つの異なるスレッドからこれを呼び出す場合、myVarの値のクエリは、addOneのすべての呼び出しが完了した後にクエリが発生するか、2つの呼び出しの間にクエリが発生するか、クエリが発生するかによって異なりますいずれかの呼び出しの前。
上記の例ではありません。
スレッドセーフは、主に保存状態に関するものです。これを行うことにより、上記の例を非スレッドセーフにすることができます。
static int myInt;
static int addOne(int someNumber){
myInt = someNumber;
return myInt +1;
}
これは、コンテキスト切り替えにより、スレッド1がmyInt = someNumber呼び出しを取得し、次にコンテキスト切り替えを取得する可能性があることを意味します。スレッド1が5に設定するとします。次に、スレッド1が再び起動すると、使用していた5の代わりにmyIntに6が返され、予想される6の代わりに7が返されます。:O
どこでも、スレッドセーフは、2つ以上のスレッドがないことを意味します。リソースにアクセスしているときに衝突します。通常、C#、VB.NET、Javaなどの言語の静的変数により、コードが thread unsafe になりました。
Javaには synchronized キーワードが存在します。しかし、.NETでは、アセンブリオプション/ディレクティブを取得します。
class Foo
{
[MethodImpl(MethodImplOptions.Synchronized)]
public void Bar(object obj)
{
// do something...
}
}
非スレッドセーフクラスの例は、このパターンのコーディング方法に応じて、シングルトンである必要があります。通常、同期インスタンス作成者を実装する必要があります。
同期メソッドが必要ない場合は、 spin-lock などのロックメソッドを試すことができます。
2つのスレッドが同時に使用できるオブジェクトへのアクセスは、スレッドセーフではありません。
パート2の例は、引数として渡された値のみを使用するため、明らかに安全ですが、オブジェクトスコープの変数を使用した場合は、アクセスを適切なロックステートメントで囲む必要があります
foo
は同時または順次呼び出し間で共有されないため、addOne
はスレッドセーフです。
この例で「foo」と「someNumber」が安全である理由は、それらがスタック上に存在し、各スレッドが独自のスタックを持っているため、共有されないためです。
グローバルなデータやオブジェクトへのポインタの共有など、データが共有される可能性があるとすぐに、競合が発生する可能性があり、何らかのロックを使用する必要がある場合があります。