Race condition, deadlock, livelock и starvation в Go
Зачем это на интервью
Интервьюер проверяет, отличаете ли вы логическую гонку от data race, умеете ли читать зависший goroutine dump и проектировать протокол остановки без «добавим mutex на всякий случай». В production эти ошибки проявляются как редкие неверные данные, исчерпание goroutine или запросы с бесконечной latency.
Минимум для E4
- Race condition — результат зависит от недетерминированного порядка событий. Она возможна и без
data race, например два конкурентных запроса проверяют остаток товара, а затем оба списывают его через отдельные транзакции. - Data race — две goroutine одновременно обращаются к одной ячейке памяти, хотя бы одна пишет, и доступы не упорядочены синхронизацией. Это ошибка по Go memory model.
- Deadlock — все участники ждут событие, которое никто из них не может произвести. Типичные причины: несовпавшие send/receive, повторный захват
Mutex, разный порядок нескольких locks. - Livelock — goroutine исполняются, но постоянно уступают/повторяют попытку и полезной работы не делают.
- Starvation — работа готова, но долго не получает CPU, lock или ресурс. Это не обязательно вечная блокировка.
- Запускайте тесты и воспроизведение с
go test -race ./...; детектор динамический: он показывает только выполненные пути и не доказывает отсутствия race.
package counter
import "sync"
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}Один и тот же mu защищает все чтения и записи n. Нельзя читать n без lock «только для логирования».
Углубление для E5/Senior
Синхронизация создаёт отношение happens-before: unlock mutex предшествует следующему lock того же mutex; send канала предшествует соответствующему receive; close канала предшествует receive, вернувшему zero value из закрытого канала. Без него компилятор и CPU вправе наблюдать записи в другом порядке.
Предотвращайте deadlock инвариантами: фиксированный глобальный порядок ресурсов, короткие критические секции, отсутствие внешнего I/O под mutex, контекст/таймаут в блокируемых операциях. sync.Mutex не reentrant; попытка вызвать метод, снова берущий тот же mutex, часто означает неверную декомпозицию API.
Планировщик Go стремится к прогрессу, но не даёт прикладной гарантии справедливости. Не строят correctness на «все когда-нибудь получат lock». Для очередей и ограничения нагрузки нужны явные очереди, bounded concurrency, метрики ожидания и cancellation. RWMutex полезен только при измеренном преобладании чтений; поток писателей может сделать его хуже обычного mutex.
При зависании снимайте goroutine profile/stack (SIGQUIT в Unix, runtime/pprof), ищите циклическую цепочку ожиданий, а не только строку, где goroutine остановилась. Для contention используйте mutex/block profile и измеряйте p95 ожидания, а не заменяйте синхронизацию atomics без доказанного протокола.
Ключевые понятия
| Термин | Признак | Основной ответ |
|---|---|---|
| Data race | -race сообщает конфликтующий read/write | общий lock, канал ownership или атомарный протокол |
| Race condition | корректность зависит от interleaving | сделать переход состояния атомарным/сериализованным |
| Deadlock | все ждут | изменить протокол и порядок ресурсов |
| Livelock | CPU работает, прогресса нет | backoff с jitter, лидер/очередь, ограничение повторов |
| Starvation | отдельная работа долго не обслуживается | fairness/очередь, лимиты и наблюдаемость |
Типовые вопросы
- Чем
data raceотличается от race condition?- Data race — нарушение модели памяти на доступе к переменной; race condition — ошибка алгоритма порядка.
Mutexуберёт data race, но не исправит проверку «прочитал баланс → позднее записал» без атомарного перехода.
- Data race — нарушение модели памяти на доступе к переменной; race condition — ошибка алгоритма порядка.
- Почему
go test -raceне является доказательством безопасности?- Инструмент динамический и видит только реально выполненные interleaving. Нужны дизайн ownership, review и тесты с нагрузкой/разными путями.
- Почему нельзя лечить deadlock
select { default: }?- Он меняет блокировку на потерю сообщения или busy loop. Нужно определить владельца, условие завершения и кто обязан получить/закрыть канал.
- Как избежать deadlock двух mutex?
- Задать тотальный порядок, например по ID, и всегда брать
minзатемmax; не держать lock во время RPC/DB-вызова.
- Задать тотальный порядок, например по ID, и всегда брать
- Может ли канал дать deadlock?
- Да: send в unbuffered channel ждёт receiver, а buffered channel блокирует sender при полном буфере.
closeне освобождает sender и не должен выполняться receiver-ом.
- Да: send в unbuffered channel ждёт receiver, а buffered channel блокирует sender при полном буфере.
- Когда starvation — реальная проблема?
- При неограниченном потоке новых задач, тяжёлой критической секции или приоритетной обработке без квоты. Наблюдают возраст очереди и время ожидания, затем вводят bounded queue/справедливую диспетчеризацию.
Практика
- Напишите тест с двумя goroutine, инкрементирующими общий счётчик без синхронизации; получите отчёт
go test -race, затем исправьте черезMutexи подтвердите чистый запуск. - Смоделируйте перевод между двумя счетами. Критерии: одинаковый порядок захвата locks, сумма не меняется, 1 000 параллельных переводов завершаются по timeout и под
-race. - Возьмите goroutine dump зависшей программы и нарисуйте граф ожиданий. Критерии: названы все блокирующие операции и единственное изменение, разрывающее цикл.
- Создайте очередь с медленным consumer. Критерии: producer не создаёт busy loop, отмена освобождает ожидающих, есть метрика длины/возраста очереди.
Частые ошибки и ловушки
- Копировать struct после первого использования содержащегося в ней
Mutex,RWMutexилиWaitGroup. - Считать, что атомарный
Load/Storeавтоматически делает последовательность из нескольких шагов транзакционной. - Держать mutex на сетевом вызове, записи в канал или callback неизвестного кода.
- Игнорировать error/timeout и принимать тест без зависания за доказательство отсутствия deadlock.
- Использовать
time.Sleepкак синхронизацию: он делает тест флейковым и скрывает протокол.
Связанные темы
Go · Fan-out, fan-in и worker pool · Pipelines и cancellation · Graceful shutdown