スレッドセーフに取り組んでいると、ロックブロックでコードを実行する前に常に「二重チェック」を行っていることに気づき、自分が正しいことをしているかどうか疑問に思いました。同じことを行う次の 3 つの方法を検討してください。
例 1:
private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
if(MyCollection[key] == null)
{
lock(locker)
{
MyCollection[key] = DoSomethingExpensive();
}
}
DoSomethingWithResult(MyCollection[key]);
}
例 2:
private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
lock(locker)
{
if(MyCollection[key] == null)
{
MyCollection[key] = DoSomethingExpensive();
}
}
DoSomethingWithResult(MyCollection[key]);
}
例 3:
private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
if(MyCollection[key] == null)
{
lock(locker)
{
if(MyCollection[key] == null)
{
MyCollection[key] = DoSomethingExpensive();
}
}
}
DoSomethingWithResult(MyCollection[key]);
}
私は常に例 3 に傾倒しています。これが、私が正しいことをしていると思う理由です。
- スレッド 1 が入ります
DoSomething(string)
MyCollection[key] == null
そのため、スレッド 2 が入るのと同じように、スレッド 1 がロックを取得します。MyCollection[key] == null
は依然として true であるため、スレッド 2 はロックの取得を待機します- スレッド 1 は値を計算
MyCollection[key]
し、コレクションに追加します - スレッド 1 はロックを解除して呼び出します
DoSomethingWithResult(MyCollection[key]);
- スレッド 2 がロックを取得します。
MyCollection[key] != null
- スレッド 2 は何もせず、ロックを解放し、引き続き楽しく作業を続けます
例 1 は機能しますが、スレッド 2 が を冗長に計算する大きなリスクがありますMyCollection[key]
。
例 2 は機能しますが、必要がなくてもすべてのスレッドがロックを取得します。これは (確かに非常に小さい) ボトルネックになる可能性があります。必要がないのに、なぜスレッドを保留にするのですか?
私はこれを考えすぎていますか?もしそうなら、これらの状況を処理するための好ましい方法は何ですか?