複数のスレッドから複数回呼び出すことができるC#の関数があり、それを1回だけ実行したいので、これについて考えました:
_class MyClass
{
bool done = false;
public void DoSomething()
{
lock(this)
if(!done)
{
done = true;
_DoSomething();
}
}
}
_
問題は、__DoSomething
_に時間がかかり、done
がtrueであることがわかるだけで、多くのスレッドが待機しないようにすることです。
このような何かが回避策になる可能性があります:
_class MyClass
{
bool done = false;
public void DoSomething()
{
bool doIt = false;
lock(this)
if(!done)
doIt = done = true;
if(doIt)
_DoSomething();
}
}
_
ただし、手動でロックおよびロック解除を行う方がはるかに優れています。lock(object)
のように手動でロックおよびロック解除するにはどうすればよいですか?この手動の方法とlock
が互いにブロックするように、lock
と同じインターフェイスを使用する必要があります(より複雑な場合)。
lock
キーワードは、Monitor.EnterとMonitor.Exitの構文糖衣です。
Monitor.Enter(o);
try
{
//put your code here
}
finally
{
Monitor.Exit(o);
}
と同じです
lock(o)
{
//put your code here
}
トーマスは彼の答えでロックをダブルチェックすることを提案しています。これは問題です。まず、ローロックテクニックによって解決される実際のパフォーマンスの問題があることを示していない限り、ローロックテクニックを使用しないでください。ローロックテクニックを正しく行うのは非常に困難です。
第二に、「_ DoSomething」が何をするのか、または依存するそのアクションの結果がわからないため、問題があります。
3番目に、上記のコメントで指摘したように、実際に別のスレッドがまだ実行中であるときに_DoSomethingが「完了」したことを返すのはおかしいようです。なぜあなたがその要件を持っているのか理解できません。それは間違いだと思います。このパターンの問題は、 "_ DoSomething"が実行した後で "done"を設定しても依然として存在します。
以下を検討してください。
class MyClass
{
readonly object locker = new object();
bool done = false;
public void DoSomething()
{
if (!done)
{
lock(locker)
{
if(!done)
{
ReallyDoSomething();
done = true;
}
}
}
}
int x;
void ReallyDoSomething()
{
x = 123;
}
void DoIt()
{
DoSomething();
int y = x;
Debug.Assert(y == 123); // Can this fire?
}
これは、C#のすべての可能な実装でスレッドセーフですか?そうではないと思います。 不揮発性読み取りは、プロセッサキャッシュによって時間内に移動できることに注意してください。 C#言語は、揮発性読み取りがロックなどの重要な実行ポイントに関して一貫して順序付けられていることを保証し、不揮発性読み取りが実行の単一スレッド内で一貫していることを保証しますが、ではありません実行スレッド全体で、不揮発性読み取りの一貫性が保証されます。
例を見てみましょう。
AlphaとBravoの2つのスレッドがあるとします。どちらもMyClassの新しいインスタンスでDoItを呼び出します。何が起こるのですか?
スレッドBravoでは、プロセッサキャッシュがたまたまゼロを含むxのメモリ位置の(非揮発性!)フェッチを実行します。 「完了」は、まだキャッシュにフェッチされていないメモリの別のページにあります。
別のプロセッサの「同時」のスレッドAlphaで、DoItはDoSomethingを呼び出します。 Thread Alphaはそこですべてを実行します。スレッドAlphaの処理が完了すると、doneがtrueになり、Alphaのプロセッサでxが123になります。スレッドアルファのプロセッサは、これらのファクトをメインメモリにフラッシュします。
スレッドブラボーがDoSomethingを実行するようになりました。 「done」を含むメインメモリのページをプロセッサキャッシュに読み込み、それが正しいことを確認します。
したがって、「done」は真ですが、スレッドBravoのプロセッサキャッシュでは「x」はまだゼロです。 スレッドブラボーでは、「完了」の読み取りも「x」の読み取りも揮発性読み取りではなかったため、「x」がゼロであるキャッシュの部分を無効にするためにスレッドブラボーは必要ありません。
提案されたバージョンのダブルチェックロックは、実際にはダブルチェックロックではありません。ダブルチェックされたロックパターンを変更する場合は、最初からやり直してeverythingを再分析する必要があります。
このバージョンのパターンを正しくする方法は、少なくとも「完了」の最初の読み取りを揮発性読み取りにすることです。次に、「x」の読み取りは、揮発性読み取りの「先」を「完了」に移動することはできません。
ロック後のandの前のdone
の値を確認できます。
_ if (!done)
{
lock(this)
{
if(!done)
{
done = true;
_DoSomething();
}
}
}
_
この方法では、done
がtrueの場合にロックに入ることはありません。ロック内の2番目のチェックは、2つのスレッドが最初のif
に同時に入る場合の競合状態に対処することです。
ところで、デッドロックが発生する可能性があるため、 this
をロックしないでください。代わりにプライベートフィールドをロックします(private readonly object _syncLock = new object()
など)。
lock
キーワードは Monitor
クラスの構文糖衣です。 Monitor.Enter()
、 Monitor.Exit()
を呼び出すこともできます。
ただし、Monitorクラス自体にも関数 TryEnter()
および Wait()
があり、状況に役立ちます。
私はこの答えが数年遅れて来ることを知っていますが、現在の答えはどれもあなたの実際のシナリオに対応していないようで、それは comment の後で初めて明らかになりました:
他のスレッドは、ReallyDoSomethingによって生成された情報を使用する必要はありません。
他のスレッドが操作が完了するのを待つ必要がない場合は、質問の2番目のコードスニペットが正常に機能します。ロックを完全に排除し、代わりにアトミック操作を使用することで、さらに最適化できます。
_private int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0) // only evaluates to true ONCE
_DoSomething();
}
_
さらに、_DoSomething()
がファイアアンドフォーゲット操作の場合、最初のスレッドがそれを待つ必要がなく、スレッドプールのタスクで非同期に実行できる場合があります。
_int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0)
Task.Factory.StartNew(_DoSomething);
}
_