いいえ、あなたが持っているものは安全ではありません。別のスレッドから_count >= MAXIMUM
の呼び出しと競合する可能性があるかどうかを確認するためのチェック。Interlocked.Increment
これは、ローロック技術を使用して解決するのは実際には非常に困難です。これを適切に機能させるには、ロックを使用せずに一連のいくつかの操作をアトミックに見せる必要があります。それが難しい部分です。ここで問題となる一連の操作は次のとおりです。
- 読んだ
_count
- テスト
_count >= MAXIMUM
- 以上を踏まえて判断してください。
- 決定
_count
に応じて増減します。
これらの 4 つのステップすべてをアトミックにしないと、競合状態が発生します。ロックを取得せずに複雑な操作を実行するための標準的なパターンは次のとおりです。
public static T InterlockedOperation<T>(ref T location)
{
T initial, computed;
do
{
initial = location;
computed = op(initial); // where op() represents the operation
}
while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
return computed;
}
何が起こっているかに注意してください。初期値が最初に読み取られてから変更が試行された時点までの間に初期値が変更されていないと ICX 操作が判断するまで、この操作は繰り返し実行されます。これは標準的なパターンであり、CompareExchange
(ICX) 呼び出しによって魔法がすべて発生します。ただし、これはABA 問題を考慮していないことに注意してください。1
あなたができること:
したがって、上記のパターンをコードに組み込むと、このような結果になります。
public void CheckForWork()
{
int initial, computed;
do
{
initial = _count;
computed = initial < MAXIMUM ? initial + 1 : initial;
}
while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
if (replacement > initial)
{
Task.Run(() => Work());
}
}
個人的には、ローロック戦略を完全にパントします。私が上で提示したものにはいくつかの問題があります。
- これは実際には、ハード ロックを取得するよりも遅くなる可能性があります。理由は説明が難しく、私の回答の範囲外です。
- 上記の内容から逸脱すると、コードが失敗する可能性があります。はい、それは本当に脆いです。
- わかりにくいです。つまり、それを見てください。それは醜いです。
あなたがすべきこと:
ハード ロック ルートを使用すると、コードは次のようになります。
private object _lock = new object();
private int _count;
public void CheckForWork()
{
lock (_lock)
{
if (_count >= MAXIMUM) return;
_count++;
}
Task.Run(() => Work());
}
public void CompletedWorkHandler()
{
lock (_lock)
{
_count--;
}
}
これははるかに単純で、エラーが発生しにくいことに注意してください。実際、このアプローチ (ハード ロック) は、上で示した方法 (ロー ロック) よりも高速であることに気付くかもしれません。繰り返しますが、理由はトリッキーで、速度を上げるために使用できる手法がありますが、この回答の範囲外です。
1この場合、ABA の問題は実際には問題ではありません。なぜなら、ロジックは変更され_count
ないままであることに依存していないからです。その間に何が起こったかに関係なく、その値が 2 つの時点で同じであることが重要です。言い換えれば、実際には値が変化しているにもかかわらず、値が変化していないように見える問題に還元することができます。