2013-02-21 6 views
6

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ści null po wywołaniu SetValue() 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 #pragma to 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.

+1

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

+0

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. –

+0

ReaderWriterLock (Slim) jest rzeczywiście lepszy niż blokada porządkowa, ponieważ pozwala na wiele równoczesnych odczytów. – Woodman

Odpowiedz

0

Żadna z twoich metod oprócz tej z lock (# 3) nie będzie działać poprawnie.

Look:

if (_value == null) { 
     throw new System.Exception("Value not yet present."); 
    } 
    return _value; 

ten kod nie jest atomowa i nie bezpieczny wątku, jeśli nie wewnątrz lock. Wciąż możliwe jest, że inne zestawy gwintów są między między i return. Co można zrobić, to ustawić na zmienną lokalną:

var localValue = _value; 
    if (localValue == null) { 
     throw new System.Exception("Value not yet present."); 
    } 
    return localValue; 

Ale nadal może zwrócić wartość przeciągnięcia. Lepiej użyj lock - wyraźnego, łatwego i szybkiego.

Edit: Unikaj lock(this), ponieważ this jest widoczna na zewnątrz i kod innej firmy może zdecydować o lock na swoim obiekcie.

Edit 2: jeśli wartość zerowa nie można ustawić tylko wtedy zrobić:

public T GetValue() { 
    if (_value == null) { 
     throw new System.Exception("Value not yet present."); 
    } 
    return _value; 
} 

public T SetValue(T newValue) { 
    lock (writeLock) 
    {   
     if (newValue == null) { 
      throw new System.ArgumentNullException(); 
     } 
     _value = newValue; 
     return newValue; 
    } 
} 
+0

Zauważ, że użycie * tego * jako celu blokady jest złym pomysłem (blokada (ta)), bezpieczniej jest zadeklarować oddzielne prywatne pole obiektu w tym celu – Woodman

+0

@Woodman, dziękuję, chciałem o tym wspomnieć, ale zapomniałem. – Andrey

+0

Nie, inny nie może ustawić wartości '_value 'na null: Moja sprawa jest wyjątkowa, ponieważ' _value' jest zapisywane tylko raz po jej inicjalizacji. Zostaje zainicjowany na 'null', a następnie w pewnym momencie zostanie ustawiony na wartość inną niż null, a następnie nigdy nie zostanie zmodyfikowany. –

1

Cóż, nie wiem, o zmienności, ale jeśli nie przeszkadza trochę nadużyciom i powołując się drugą metodę. .. (także nie jest zależne od nullability, swobodnie użyteczne dla typów wartości) Unika też sprawdzeń zerowych w geterze.Tylko blokowanie odbywa się przy zapisie, więc AFAIK, jedynym negatywnym skutkiem jest wywołanie delegata podczas pobierania wartości.

public class SetOnce<T> 
{ 
    private static readonly Func<T> NoValueSetError =() => { throw new Exception("Value not yet present.");}; 

    private Func<T> ValueGetter = NoValueSetError; 
    private readonly object SetterLock = new object(); 

    public T SetValue(T newValue) 
    { 
     lock (SetterLock) 
     { 
      if (ValueGetter != NoValueSetError) 
       throw new Exception("Value already present."); 
      else 
       ValueGetter =() => newValue; 
     } 

     return newValue; 
    } 

    public T GetValue() 
    { 
     return ValueGetter(); 
    } 
} 

W rzeczywistości czuję się naprawdę głupio i czuję się nieco obraźliwy. Byłbym ciekawy komentarzy na temat potencjalnych problemów z tym związanych. :)

EDYCJA: Właśnie zdałem sobie sprawę, że oznacza to, że pierwsze połączenie z SetValue(null) oznacza, że ​​"null" będzie uważane za prawidłową wartość i zwróci wartość zerową bez wyjątku. Nie wiem, czy tego właśnie chcesz (nie rozumiem, dlaczego null nie może być prawidłową wartością, ale jeśli chcesz tego uniknąć, po prostu sprawdź w setera, nie ma potrzeby w pobierającym)

EDITx2: Jeśli nadal chcesz, aby ograniczyć je do class i uniknąć null wartości, prosta zmiana może być:

public class SetOnce<T> where T : class 
{ 
    private static readonly Func<T> NoValueSetError =() => { throw new Exception("Value not yet present.");}; 

    private Func<T> ValueGetter = NoValueSetError; 
    private readonly object SetterLock = new object(); 

    public T SetValue(T newValue) 
    { 
     if (newValue == null) 
      throw new ArgumentNullException("newValue"); 

     lock (SetterLock) 
     { 
      if (ValueGetter != NoValueSetError) 
       throw new Exception("Value already present."); 
      else 
       ValueGetter =() => newValue; 
     } 

     return newValue; 
    } 

    public T GetValue() 
    { 
     return ValueGetter(); 
    } 
} 
+0

Głównym problemem, jaki widzę, jest to, że w GetValue można uzyskać nieaktualną wartość (NoValueSetError) po wywołaniu SetValue w innym wątku. Poza tym twoje rozwiązanie jest dość zabawne i pomysłowe :) Bardzo mi się podobało czytanie! –

+0

@ GeorgesDupéron Tak, to właśnie mam na myśli zmienność. Może to można rozwiązać, zaznaczając pole 'ValueGetter' jako zmienne? Szczerze mówiąc, nie miałem potrzeby naprawdę pracować z niestabilnymi polami (trzymam się głównie blokad). Pomyślałem, że jeśli zdarzy ci się trafić 'GetValue' podczas wykonywania' SetValue', to co jeszcze możesz zrobić? w tym momencie? EDYCJA: Jeśli ktoś może potwierdzić, że oznaczenie pola jako "zmienny" lub inne usprawnienia omija ten możliwy "nieaktualny" problem, to z przyjemnością edytuję odpowiedź. –

Powiązane problemy