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:
- JetBrains ReSharper — inspekcje łapią część niebezpiecznych wzorców.
- Roslyn Analyzers — statyczna analiza kodu.
- Concurrency Visualizer — analiza oczekiwań/blokad i obciążenia wątków.
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ć |
|---|---|---|---|---|---|
|
Prosta sekcja krytyczna | 1 | Nie | Bardzo wysoka | 99% przypadków |
|
Ta sama sekcja, ale między procesami | 1 | Tak | Średnia | Pliki, IPC |
|
Nie więcej niż N wątków | N | Tak | Średnia | Pule zasobów |
|
To samo, ale szybsze, wewnątrz procesu | N | Nie | Wysoka | Pule w kodzie |
|
Wielu czytelników, jeden pisarz | Wielu/1 | Nie | Wysoka | Cache, konfiguracje |
GO TO FULL VERSION