2 つのメソッドを持つクラスを作成したいと思います。
void SetValue(T value)
値を保存しますが、単一の値のみを保存できます (それ以外の場合は例外がスローされます)。T GetValue()
値を取得します (まだ値がない場合は例外をスローします)。
私には次の欲求/制約があります。
- 値の読み取りは安価なはずです。
- 値の書き込みは (適度に) コストがかかる場合があります。
GetValue()
最新の値が存在しない場合にのみ例外をスローする必要があります ( ):別のスレッドでを呼び出した後、null
古い値に基づいて例外をスローするべきではありません。null
SetValue()
- 値は一度だけ書き込まれます。これは
GetValue()
、値が null でない場合、値を更新する必要がないことを意味します。 - 完全なメモリ バリアを回避できる場合は、(はるかに) 優れています。
- ロックフリーの並行性の方が優れていることはわかりますが、ここでそうであるかどうかはわかりません。
私はそれを達成するためのいくつかの方法を思いつきましたが、どれが正しいのか、どれが効率的なのか、なぜそれらが (不) 正しくて (非) 効率的なのか、そして私が望むものを達成するためのより良い方法があるかどうかはわかりません.
方法 1
- 不揮発性フィールドの使用
Interlocked.CompareExchange
フィールドへの書き込みに使用するInterlocked.CompareExchange
フィールドから読み取るために使用するInterlocked.CompareExchange(ref v, null, null)
これは、フィールドに対して を実行した後、次のアクセスで、少なくとも見た値と同じくらい新しい値が取得されるという (おそらく間違った) 仮定に依存していInterlocked.CompareExchange
ます。
コード:
public class SetOnce1<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
// Read an up-to-date value of that variable
Interlocked.CompareExchange<T>(ref _value, null, null);
// _value contains up-to-date data, because of the Interlocked.CompareExchange call above.
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
}
// _value contains up-to-date data here too.
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (Interlocked.CompareExchange<T>(ref _value, newValue, null) != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
方法 2
volatile
フィールドの使用- Ìnterlocked.CompareExchange
to write the value (with [Joe Duffy](http://www.bluebytesoftware.com/blog/PermaLink,guid,c36d1633-50ab-4462-993e-f1902f8938cc.aspx)'s
#pragmato avoid the compiler warning on passing a volatile value by
ref`) を使用します。 - フィールドが
volatile
コード:
public class SetOnce2<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
#pragma warning disable 0420
T oldValue = Interlocked.CompareExchange<T>(ref _value, newValue, null);
#pragma warning restore 0420
if (oldValue != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
方法 3
- 不揮発性フィールドの使用。
- 書き込み時にロックを使用します。
- null を読み取る場合は、読み取り時にロックを使用します (参照はアトミックに読み取られるため、ロックの外で一貫した値を取得します)。
コード:
public class SetOnce3<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
lock (this) {
// Read an up-to-date value of that variable
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
方法 4
- volatile フィールドの使用
- ロックを使用して値を書き込みます。
- フィールドは揮発性であるため、値を直接読み取ります (参照はアトミックに読み取られるため、ロックを使用しなくても一貫した値を取得できます)。
コード:
public class SetOnce4<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
その他の方法
Thread.VolatileRead()
また、任意の書き込み手法と組み合わせて、値を読み取るために使用することもできます。