Jak zamrozić popsicle w .NET (uczynić klasę niezmienną)

Projektuję klasę, którą chcę zrobić tylko po zakończeniu konfigurowania głównego wątku, tj. „Zamrożenia” go. Eric Lippert to nazywaLód na patyku niezmienność. Po zamrożeniu można uzyskać do niego dostęp przez wiele wątków jednocześnie do odczytu.

Moje pytanie brzmi: jak to napisać w bezpieczny sposób w wątkurealistycznie wydajny, tj. bez próby niepotrzebnegosprytny.

Próba 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 wydaje się sugerować, że wszystko będzie dobrzeto słupek. Wiem, że pisanie ma semantykę wydania, ale o ile rozumiem, dotyczy to tylkozamawianiei nie musi to oznaczać, że wszystkie wątki zobaczą wartość natychmiast po zapisaniu. Czy ktoś może to potwierdzić? Oznaczałoby to, że to rozwiązanie nie jest bezpieczne dla wątków (oczywiście nie jest to jedyny powód).

Próba 2:

Powyższe, ale za pomocąInterlocked.Exchange aby upewnić się, że wartość jest faktycznie opublikowana:

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

Zaletą byłoby to, że zapewniamy publikację wartości bez ponoszenia kosztów ogólnych przy każdym czytaniu. Jeśli żaden z odczytów nie zostanie przeniesiony przed zapisaniem do _isFrozen, ponieważ metoda Interlocked wykorzystuje pełną barierę pamięci, sądzę, że jest to bezpieczny wątek. Jednak kto wie, co zrobi kompilator (i zgodnie z sekcją 3.10 specyfikacji C #, która wydaje się całkiem spora), więc nie wiem, czy jest to ochrona wątków.

Próba 3:

Wykonaj także odczytInterlocked.

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

Zdecydowanie bezpieczny w wątku, ale wydaje się trochę marnotrawny, gdy trzeba wykonać wymianę porównania dla każdego odczytu. Wiem, że ten koszt jest prawdopodobnie minimalny, ale szukamrozsądnie skuteczna metoda (choć może to jest to).

Próba 4:

Za pomocąvolatile:

public class Foobar
{
   private volatile Boolean _isFrozen;

   public void Freeze() { _isFrozen = true; }

   public void WriteValue(Object val)
   {
      if (_isFrozen)
         throw new InvalidOperationException();

      // write ...
   }
}

Ale Joe Duffy oświadczył „sayonara lotna„, więc nie rozważę tego jako rozwiązania.

Próba 5:

Zablokuj wszystko, wydaje się nieco przesadzone:

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

Wydaje się również zdecydowanie bezpieczny dla wątków, ale ma więcej narzutów niż użycie powyższego podejścia Interlocked, więc wolałbym próbę 3 nad tą.

A potem mogę wymyślić przynajmniej trochę więcej (jestem pewien, że jest ich znacznie więcej):

Próba 6: posługiwać sięThread.VolatileWrite iThread.VolatileRead, ale te są podobno trochę ciężkie.

Próba 7: posługiwać sięThread.MemoryBarrier, wydaje się trochę zbytwewnętrzny.

Próba 8: stwórz niezmienną kopię - nie chcesz tego robić

Zreasumowanie:

jakiej próby byś użył i dlaczego (lub jak byś to zrobił, gdyby był zupełnie inny)? (tj. jaki jest najlepszy sposób na opublikowanie wartości, która jest następnie odczytywana jednocześnie, a jednocześnie jest racjonalnie wydajna bez nadmiernego „sprytu”)?czy semantyka zapisu modelu pamięci .NET oznacza, że ​​wszystkie inne wątki widzą aktualizacje (spójność pamięci podręcznej itp.)? Na ogół nie chcę o tym myśleć zbyt wiele, ale miło jest mieć zrozumienie.

EDYTOWAĆ:

Być może moje pytanie nie było jasne, ale w szczególności szukam powodów, dla których powyższe próby są dobre lub złe. Zauważ, że mówię tutaj o scenariuszu, w którym jeden pisarz, który pisze, zawiesza się przed odczytami współbieżnymi. Uważam, że próba 1 jest OK, ale chciałbym dokładnie wiedzieć, dlaczego (ponieważ zastanawiam się, czy na przykład odczyty mogą zostać zoptymalizowane). Nie zależy mi na tym, czy jest to dobra praktyka projektowa, ale bardziej na faktyczny aspekt gwintowania.

Wielkie dzięki za odpowiedź, którą otrzymało pytanie, ale sam postanowiłem zaznaczyć to jako odpowiedź, ponieważ uważam, że udzielone odpowiedzi niecałkiem odpowiedz na moje pytanie i nie chcę sprawiać wrażenia każdemu, kto odwiedza witrynę, że zaznaczona odpowiedź jest poprawna, ponieważ została automatycznie oznaczona jako taka z powodu wygasania nagrody. Ponadto nie sądzę, aby na odpowiedź, która otrzymała największą liczbę głosów, głosowano zdecydowanie za mało, by oznaczyć ją automatycznie jako odpowiedź.

Nadal pochylam się nad próbą poprawnego # 1, jednak chciałbym uzyskać kilka autorytatywnych odpowiedzi. Rozumiem, że x86 ma silny model, ale nie chcę (i nie powinienem) kodować konkretnej architektury, w końcu to jedna z miłych rzeczy na temat .NET.

Jeśli masz wątpliwości co do odpowiedzi, skorzystaj z jednego z podejść blokujących, być może z przedstawionymi tu optymalizacjami, aby uniknąć sporu o blokadę.

questionAnswers(12)

yourAnswerToTheQuestion