Chciałbym utworzyć klasę z dwóch metod:Czytaj up-to wartość daty od zmiennej zblokowane, z jednym tylko pisać na zmiennej
void SetValue(T value)
przechowuje wartość, ale tylko umożliwia przechowywanie pojedyncza wartość (w przeciwnym razie zgłasza wyjątek).T GetValue()
pobiera wartość (i zgłasza wyjątek, jeśli nie ma jeszcze wartości).
Mam następujące pragnienia/ograniczenia:
- Czytając wartość powinna być tania.
- Zapisywanie wartości może być (umiarkowanie) kosztowne.
GetValue()
powinny rzucać wyjątek tylko wtedy, gdy wartość up-to-date jest nieobecny (null
): To nie powinno wyjątek oparty na nieświeży wartościnull
po wywołaniuSetValue()
w innym wątku.- Wartość zostanie zapisana tylko raz. Oznacza to, że
GetValue()
nie musi odświeżać wartości, jeśli nie jest pusta. - Jeśli można uniknąć pełnej bariery pamięci, wówczas jest (znacznie) lepsza.
- Rozumiem, że ta bezkonfliktowa współbieżność jest lepsza, ale nie jestem pewien, czy tak jest w tym przypadku.
wymyśliłem kilka sposobów, aby to osiągnąć, ale nie jestem pewien, które są poprawne, które są skuteczne, to dlaczego oni są (w) prawidłowe i (w) skuteczny, a jeśli istnieje lepszy sposób osiągnąć to, czego chcę.
Metoda 1
- Korzystanie z nieulotną pole
- Korzystanie
Interlocked.CompareExchange
napisać do pola - Korzystanie
Interlocked.CompareExchange
czytać z pola - ta opiera się na (być złym) założeniu, to, że po wykonaniu
Interlocked.CompareExchange(ref v, null, null)
na polu spowoduje, że następny dostęp uzyska wartości, które są co najmniej tak aktualne, jak te, które zobaczyłInterlocked.CompareExchange
.
Kod:
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;
}
}
Metoda 2
- Stosując
volatile
pole - Stosując Ì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`). - Czytając wartość bezpośrednio, ponieważ pole jest
volatile
Kod:
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;
}
}
Metoda 3
- Korzystanie pole nieulotnej.
- Używanie zamka podczas pisania.
- Używanie blokady podczas odczytu, jeśli odczytamy wartość null (otrzymamy spójną wartość poza blokadą, ponieważ odwołania są odczytywane atomowo).
Kod:
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;
}
}
}
Metoda 4
- Korzystanie lotny pole
- Zapis wartości za pomocą zamka.
- Bezpośrednie odczytanie wartości, ponieważ pole jest zmienne (otrzymamy spójną wartość, nawet jeśli nie używamy blokady, ponieważ odniesienia są odczytywane atomowo).
Kod:
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;
}
}
}
Inne metody
mogę również użyć Thread.VolatileRead()
odczytać wartość w połączeniu z dowolnymi technikami pisania.
Czy można uznać za pomocą 'ReaderWriterLock (Slim)'? Czy jesteś pewien, że prosty "zamek" nie spełnia twoich potrzeb? Dlaczego nie? Jaki jest wzorzec użycia dla klasy 'SetOnce'? – dtb
Zapomniałem o "ReaderWriterLock", dam mu zamek. Ale tak jak zwykłe 'lock', myślę, że wprowadzi on obciążenie synchronizacyjne przy każdym odczycie, którego wolałbym uniknąć, ponieważ wartość jest zmieniana tylko raz, więc podczas odczytywania wartości innej niż null (bez blokad), jeden ma gwarancja, że nie jest nieaktualna. –
ReaderWriterLock (Slim) jest rzeczywiście lepszy niż blokada porządkowa, ponieważ pozwala na wiele równoczesnych odczytów. – Woodman