メインスレッドの構成が完了した後、つまり「フリーズ」した後に読み取り専用にしたいクラスを設計しています。Eric Lippertは、これをアイスキャンデーの不変性と呼んでいます。フリーズした後は、複数のスレッドから同時にアクセスして読み取ることができます。
私の質問は、これをスレッドセーフな方法で、現実的に効率的な方法で、つまり不必要に賢くしようとせずに書く方法です。
試行1:
public class Foobar
{
private Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
// Only intended to be called by main thread, so checks if class is frozen. If it is the operation is invalid.
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
public Object ReadSomething()
{
return it;
}
}
Eric Lippertは、この投稿でこれで問題ないと示唆しているようです。書き込みにはリリースセマンティクスがあることは知っていますが、これは順序付けにのみ関係することを理解している限り、必ずしもすべてのスレッドが書き込みの直後に値を確認することを意味するわけではありません。誰かがこれを確認できますか?これは、このソリューションがスレッドセーフではないことを意味します(もちろん、これが唯一の理由ではない可能性があります)。
試行2:
上記ですがInterlocked.Exchange
、値が実際に公開されていることを確認するために使用します。
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (_isFrozen == 1)
throw new InvalidOperationException();
// write ...
}
}
ここでの利点は、読み取りのたびにオーバーヘッドが発生することなく、値が確実に公開されることです。Interlockedメソッドは完全なメモリバリアを使用するため、_isFrozenへの書き込みの前に読み取りが移動されない場合、これはスレッドセーフであると思います。ただし、コンパイラが何をするかは誰にもわかりません(C#仕様のセクション3.10によると、かなり多くのように思われます)。したがって、これがスレッドセーフかどうかはわかりません。
試行3:
また、を使用して読み取りを行いますInterlocked
。
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
throw new InvalidOperationException();
// write ...
}
}
間違いなくスレッドセーフですが、読み取りごとに比較交換を行う必要があるのは少し無駄に思えます。私はこのオーバーヘッドがおそらく最小限であることを知っていますが、私は適度に効率的な方法を探しています(おそらくこれはそれですが)。
試行4:
使用volatile
:
public class Foobar
{
private volatile Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
しかし、ジョー・ダフィーは「さよなら揮発性」と宣言したので、これを解決策とは考えません。
試行5:
すべてをロックし、少しやり過ぎのようです:
public class Foobar
{
private readonly Object _syncRoot = new Object();
private Boolean _isFrozen;
public void Freeze() { lock(_syncRoot) _isFrozen = true; }
public void WriteValue(Object val)
{
lock(_syncRoot) // as above we could include an attempt that reads *without* this lock
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
また、間違いなくスレッドセーフのようですが、上記のインターロックアプローチを使用するよりもオーバーヘッドが大きいため、これよりも試行3を優先します。
そして、私は少なくとももう少し思いつくことができます(私はもっとたくさんあると確信しています):
試行6:とを使用Thread.VolatileWrite
しますThread.VolatileRead
が、これらはおそらく少し重い面です。
試行7:使用Thread.MemoryBarrier
、少し内部的すぎるようです。
試行8:不変のコピーを作成します-これを実行したくない
要約:
- どの試みを使用しますか、またその理由(または、まったく異なる場合はどのように実行しますか)?(つまり、値を一度公開して同時に読み取るための最良の方法は何ですか?ただし、過度に「賢く」なることなく、適度に効率的です)。
- .NETのメモリモデルの書き込みの「リリース」セマンティクスは、他のすべてのスレッドが更新(キャッシュコヒーレンシなど)を参照することを意味しますか?私は一般的にこれについてあまり考えたくないのですが、理解しておくのはいいことです。
編集:
おそらく私の質問は明確ではありませんでしたが、私は特に上記の試みが良いか悪いかについての理由を探しています。ここでは、同時読み取りの前に書き込みを行ってからフリーズする1つのライターのシナリオについて話していることに注意してください。試行1は問題ないと思いますが、その理由を正確に知りたいと思います(たとえば、読み取りを何らかの方法で最適化できるかどうか疑問に思います)。私はこれが良い設計慣行であるかどうかについては気にしませんが、それの実際の糸の側面についてはもっと気にします。
質問への回答に感謝しますが、私はこれを自分で回答としてマークすることにしました。これは、与えられた回答が私の質問に完全に答えていないように感じ、サイトにアクセスした人にマークを付けた印象を与えたくないためです。バウンティの有効期限が切れたために自動的にそのようにマークされたという理由だけで、答えは正しいです。さらに、投票数が最も多い回答が圧倒的に投票されたとは思いません。自動的に回答としてマークするのに十分ではありません。
私はまだ#1が正しいことを試みることに傾いています、しかし、私はいくつかの権威ある答えが好きでした。x86には強力なモデルがあることは理解していますが、特定のアーキテクチャー用にコーディングしたくない(そしてコーディングすべきではありません)。結局のところ、それは.NETの優れた点の1つです。
答えがわからない場合は、ロックの方法の1つを選択してください。おそらく、ここに示す最適化を使用して、ロックに関する多くの競合を回避します。