CodeGym /Kursy /C# SELF /Najlepsze praktyki i narzędzia do diagnostyki

Najlepsze praktyki i narzędzia do diagnostyki

C# SELF
Poziom 57 , Lekcja 4
Dostępny

1. Sekret odpornego wielowątkowego kodu

Jeżeli wyobrazić sobie wielowątkowość jako zespół kilku pracowników jednocześnie naprawiających samochód, to jasne: wystarczy, że jeden z nich chwyci cudze narzędzie — i naprawa stanie. W kodzie jest tak samo: nieostrożna praca z danymi współdzielonymi prowadzi do ukrytych błędów, które mogą ujawnić się dopiero "w boju".

W tej lekcji — jak pisać wielowątkowy kod, który nie rozsypuje się jak domek z kart. I jakie narzędzia pomogą, gdy coś pójdzie nie tak.

1. Minimalizuj krytyczne sekcje (lock)

Im mniej kodu znajduje się wewnątrz blokady lock, tym lepiej. Dopóki jeden wątek trzyma zamek, reszta czeka.

Przykład:


// ZLE: Cała logika biznesowa wewnątrz lock — wszystkie wątki czekają
lock(_locker)
{
    // Długa operacja (nie związana ze współdzielonym zasobem)
    Thread.Sleep(500);
    counter++;
}

// DOBRZE: Tylko minimalne niezbędne działanie
// wewnątrz lock — cała ciężka praca poza blokadą
Thread.Sleep(500);
lock(_locker)
{
    counter++;
}

Rzeczywistość: Jeśli w blokadzie znajdzie się wywołanie sieciowe lub długi obliczeń, wydajność spada drastycznie.

2. Nie używaj jako klucza blokady obiektów uniwersalnych

Pisanie lock(this) albo lock(typeof(MyClass)) — to zły pomysł.

Dlaczego? Jeśli ktoś inny użyje tego samego obiektu do swojej blokady, dostaniesz deadlocky lub ukryte bugi. Zawsze używaj oddzielnego prywatnego obiektu:

private readonly object _locker = new object();

lock(_locker)
{
    // Twoje działania
}

Zakazane: stringi (string), pola publiczne, obiekty typu-wartości.

3. Zawsze używaj try...finally do zwalniania przejętych zasobów

Każde przejęcie Mutex, semafora, ReaderWriterLockSlim — musi być zwolnione w finally.

_mutex.WaitOne();
try
{
    // Sekcja krytyczna
}
finally
{
    _mutex.ReleaseMutex();
}

4. Nie nadużywaj synchronizacji

Synchronizuj tylko dostęp do prawdziwych zasobów współdzielonych (np. kolekcji), a nie "każdy kich". Nadmiarowe blokady zamieniają kod w kolejkę oczekiwania.

5. Używaj thread-safe kolekcji i typów

.NET dostarcza specjalne kolekcje do scenariuszy wielowątkowych: ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, BlockingCollection i inne. One same się wewnątrz zabezpieczają.

using System.Collections.Concurrent;

ConcurrentDictionary<int, string> users = new ConcurrentDictionary<int, string>();
users.TryAdd(1, "User1");
users[2] = "User2";

6. Uważaj na deadlock (wzajemne blokowanie)

Typowa pułapka — przejmowanie wielu zamków w różnej kolejności.

// Wątek 1
lock(obj1)
{
    lock(obj2)
    {
        // Robimy coś
    }
}

// Wątek 2
lock(obj2)
{
    lock(obj1)
    {
        // Robimy coś
    }
}

Wskazówka: Zawsze przejmuj blokady w tej samej kolejności we wszystkich wątkach.

7. Jeśli to możliwe, używaj niemodyfikowalnego stanu (immutable state)

Jeśli obiekt nie zmienia stanu po utworzeniu — można go bezpiecznie czytać z dowolnych wątków. Przykłady: string, Tuple, DateTime, własne DTO tylko do odczytu.

2. Narzędzia do diagnostyki problemów wielowątkowości

Błędy synchronizacji są podstępne: pojawiają się rzadko i nieprzewidywalnie. Używaj narzędzi i podejść, które pomagają je łapać i analizować.

1. Logowanie zdarzeń i wątków

Loguj aktualny Thread.ManagedThreadId i kluczowe operacje — to prosty sposób, żeby zrozumieć "kto i kiedy" wszedł/wyszedł z sekcji krytycznej.

Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Wszedł do sekcji krytycznej");
// ...
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Wyszedł z sekcji krytycznej");

Dla produkcyjnych aplikacji używaj Microsoft.Extensions.Logging, NLog, Serilog.

2. Thread Sanitizer & Race Detector

W .NET nie ma "idealnego" wbudowanego ThreadSanitizer, ale są przydatne narzędzia:

3. Visual Studio Diagnostics Tools

Profile Visual Studio pomagają zobaczyć:

  • jakie wątki istnieją w aplikacji;
  • gdzie wątki stoją w miejscu (waiting);
  • gdzie powstają blokady i rywalizacja o zamki;
  • kiedy pojawiają się deadlock i contention.

Nagrywaj ślady, żeby uzyskać szczegółowy graf użycia blokad.

4. Analiza zrzutów i WinDbg

Jeśli serwer "zawisł", zrób zrzut procesu i otwórz go w WinDbg lub dotnet-dump. Po stosach wywołań widać, gdzie wątki utknęły i kto trzyma jakie zamki.

Przykład analizy stosów:

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 000001d4b6f90e08          1         1 000001d4b5c941c0 000001d4b6f03458

(Do zrzutów zwykle sięgają "doświadczeni jedi" deploymentu — nie bój się, to potężne narzędzie.)

5. Unit-testy z obciążeniem (stress testing)

Uruchamiaj wielowątkowy kod równolegle w setkach/tysiącach iteracji — wtedy rzadkie race'y łatwiej znaleźć.

[Test]
public void Counter_IsThreadSafe()
{
    var counter = 0;
    var locker = new object();
    var tasks = new List<Task>();
    for (int i = 0; i < 100; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            for (int j = 0; j < 10000; j++)
            {
                lock (locker)
                {
                    counter++;
                }
            }
        }));
    }
    Task.WaitAll(tasks.ToArray());
    Assert.AreEqual(100 * 10000, counter);
}

6. Użycie assertów i specjalnych checków

Dodawaj walidacje, które gwarantują poprawny stan w debugowaniu. Na przykład, Debug.Assert przy próbie ponownego przejęcia zasobu przez ten sam wątek.

3. Wnioski i rekomendacje

Wizualna schemat: strefa niebezpieczna i bezpieczeństwo

graph TD
    A[Wspólny zasób] -- bez synchronizacji --> B(Stan race)
    A -- blokada (lock/Mutex) --> C[Bezpieczny dostęp: sekcja krytyczna]
    C -- "za dużo blokad" --> D(Utrata wydajności)
    A -- ReaderWriterLockSlim --> E{Wielu czytelników / Jeden pisarz}
    E -- "Czytanie" --> F[Wiele wątków czyta jednocześnie]
    E -- "Zapisywanie" --> G[Tylko jeden zapisuje, reszta czeka]

Prymitywy synchronizacyjne i ich przeznaczenie

Prymityw Do czego służy Ilu wątków przepuszcza Międzyprocesowo Wydajność Gdzie używać
lock (Monitor)
Prosta sekcja krytyczna 1 Nie Bardzo wysoka 99% przypadków
Mutex
Ta sama sekcja, ale między procesami 1 Tak Średnia Pliki, IPC
Semaphore
Nie więcej niż N wątków N Tak Średnia Pule zasobów
SemaphoreSlim
To samo, ale szybsze, wewnątrz procesu N Nie Wysoka Pule w kodzie
ReaderWriterLockSlim
Wielu czytelników, jeden pisarz Wielu/1 Nie Wysoka Cache, konfiguracje
1
Ankieta/quiz
Wzajemne blokady, poziom 57, lekcja 4
Niedostępny
Wzajemne blokady
Typowe problemy wielowątkowości
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION