Sobes.tech
Middle+

Jak wygląda obsługa wątków i zadań w C#, gdy kilka wątków odwołuje się do wspólnego stanu?

sobes.tech AI

Odpowiedź od AI

Przy pracy ze stanem współdzielonym przez wiele wątków w C# mogą wystąpić problemy z synchronizacją i warunki wyścigu. Aby zapewnić bezpieczeństwo wątków, używa się następujących mechanizmów:

  1. Blokada (Locking):

    • Operator lock jest używany do tworzenia sekcji krytycznej, która gwarantuje, że tylko jeden wątek może uzyskać dostęp do chronionego zasobu w danym czasie.
    • Wymaga obiektu blokady (zwykle private readonly object), który jest używany do synchronizacji.
    // Przykład użycia lock
    private readonly object _lockObject = new object();
    private int _sharedCounter = 0;
    
    public void IncrementCounter()
    {
        lock (_lockObject)
        {
            _sharedCounter++; // Sekcja krytyczna
        }
    }
    
  2. Operacje atomowe:

    • Klasa System.Threading.Interlocked zapewnia operacje atomowe (np. Increment, Decrement, Add, Exchange), które są bezpieczne dla wątków i nie wymagają jawnego blokowania do wykonania jednej operacji.
    // Przykład użycia Interlocked
    private volatile int _sharedValue = 0; // volatile dla widoczności zmian
    
    public void UpdateValue(int newValue)
    {
        Interlocked.Exchange(ref _sharedValue, newValue); // Atomowe przypisanie
    }
    
    public void IncrementValue()
    {
        Interlocked.Increment(ref _sharedValue); // Atomowe zwiększenie
    }
    
    • Chociaż volatile zapewnia widoczność zmian między wątkami, nie gwarantuje atomowości operacji wieloetapowych. Do operacji atomowych należy używać Interlocked.
  3. Klasy synchronizacji z System.Threading i System.Threading.Tasks:

    • Monitor: niższy poziom mechanizmu blokowania w porównaniu do lock.
    • Mutex: pozwala synchronizować dostęp do zasobu między wieloma procesami.
    • SemaphoreSlim: ogranicza liczbę wątków, które mogą jednocześnie uzyskać dostęp do zasobu.
    • ReaderWriterLockSlim: pozwala wielu wątkom czytać zasób jednocześnie, ale tylko jednemu pisać.
  4. Kolekcje bezpieczne dla wątków:

    • Namespace System.Collections.Concurrent zawiera kolekcje (ConcurrentBag<T>, ConcurrentDictionary<TKey, TValue>, ConcurrentQueue<T>, ConcurrentStack<T>), zaprojektowane specjalnie do dostępu współbieżnego. Użycie tych kolekcji eliminuje konieczność ręcznej synchronizacji przy podstawowych operacjach na kolekcji.
    // Przykład użycia ConcurrentDictionary
    private ConcurrentDictionary<string, int> _sharedData = new ConcurrentDictionary<string, int>();
    
    public void AddOrUpdateData(string key, int value)
    {
        _sharedData.AddOrUpdate(key, value, (k, oldValue) => oldValue + value);
    }
    
  5. Obiekty niemutowalne:

    • Tworzenie obiektów niemutowalnych oznacza, że po ich utworzeniu ich stan nie może zostać zmieniony. Jeśli stan współdzielony jest reprezentowany przez obiekt niemutowalny, wiele wątków może go bezpiecznie czytać bez konieczności synchronizacji. Aktualizacja stanu sprowadza się do utworzenia nowej instancji obiektu.

Przy pracy z zadaniami (Task) w TPL (Task Parallel Library) należy również rozważyć synchronizację przy dostępie do stanu współdzielonego. TPL upraszcza tworzenie i zarządzanie operacjami równoległymi, ale nie zwalnia z odpowiedzialności za zapewnienie bezpieczeństwa wątków podczas pracy z danymi współdzielonymi.

Wybór odpowiedniego mechanizmu synchronizacji zależy od konkretnego zadania i cech dostępu do stanu współdzielonego.