Race condition, deadlock, livelock, starvation
↑ 2.3 Асинхронность и многопоточность · ← Предыдущая · → Следующая
Зачем это на собесе
Вопросы вида «приведи пример deadlock и как его диагностировать» задают почти всем. Нужно показать понимание условий возникновения и практических способов борьбы.
Объяснение
| Проблема | Суть | Симптом |
|---|---|---|
| Race condition | результат зависит от порядка выполнения потоков | плавающие неверные данные, «иногда падает» |
| Data race | одновременный доступ к памяти без синхронизации, хотя бы одна запись | порча данных, torn reads |
| Deadlock | потоки ждут друг друга бесконечно | зависание, 0% CPU |
| Livelock | потоки активно уступают друг другу, но прогресса нет | высокий CPU, ничего не происходит |
| Starvation | поток годами не получает ресурс | отдельные задачи «застревают» |
Race condition: check-then-act
// Плохо: между проверкой и записью может вклиниться другой поток
if (!_cache.ContainsKey(key))
_cache[key] = Load(key);
// Хорошо
var value = _concurrentCache.GetOrAdd(key, k => Load(k));Обратите внимание: GetOrAdd с фабрикой может вызвать фабрику несколько раз. Если фабрика дорогая — храните Lazy<T>.
Deadlock: четыре условия Коффмана
- Взаимное исключение.
- Удержание и ожидание (hold and wait).
- Отсутствие принудительного освобождения.
- Циклическое ожидание.
Достаточно сломать любое из них.
// Поток A: lock(a) -> lock(b); поток B: lock(b) -> lock(a)
void Transfer(Account from, Account to)
{
lock (from) lock (to) { /* ... */ }
}
// Исправление: единый порядок захвата
void TransferSafe(Account from, Account to)
{
var (first, second) = from.Id < to.Id ? (from, to) : (to, from);
lock (first) lock (second) { /* ... */ }
}Отдельно — async-deadlock: блокирующий .Result в контексте с однопоточным SynchronizationContext (см. Антипаттерны: async void, .Result, sync over async).
Livelock и starvation
Livelock — два вежливых человека в коридоре, каждый уступает дорогу в ту же сторону; лечится случайной задержкой (jitter) или приоритетом. Starvation — несправедливые блокировки, длинные задачи, вытесняющие короткие; лечится честными очередями, лимитами и приоритетами.
Нюансы и подводные камни
- Гонки не воспроизводятся стабильно: тестируйте стресс-тестами и с разным числом потоков.
Monitor.TryEnter(obj, timeout)иSemaphoreSlim.WaitAsync(timeout)превращают deadlock в диагностируемую ошибку.- Неизменяемые данные и передача сообщений (Channels, actor-подход) исключают большинство гонок.
- Диагностика:
dotnet-dump analyze+clrstack -all,dotnet-stack, окно Parallel Stacks в Visual Studio.
Практика
- Напишите минимальный deadlock на двух замках, снимите dump и найдите его в
clrstack. - Исправьте счётчик с
x++из 8 потоков черезInterlocked. - Сымитируйте starvation: длинные задачи в ThreadPool и короткая, которая не выполняется.
Вопросы с ответами
Чем race condition отличается от data race?
Data race — низкоуровневое: конфликтующие обращения к памяти без синхронизации. Race condition — логическая ошибка порядка, она возможна и без data race (например, атомарные операции, но неверная последовательность).
Как предотвратить deadlock?
Единый порядок захвата, таймауты, минимальная область замка, отказ от вложенных замков, использование неблокирующих структур и передачи сообщений.
Как найти deadlock в проде?
Снять dump процесса (
dotnet-dump collect), посмотреть стеки всех потоков и цепочку ожидания замков; в Visual Studio — Parallel Stacks и Threads.
Что такое thread-safe и reentrant?
Thread-safe — корректная работа при одновременном вызове из нескольких потоков. Reentrant — безопасный повторный вход из того же потока или в прерванном состоянии; это разные свойства.