Go: как найти deadlock на примере задачи пяти философов
Есть классическая задача из пяти слов: Computer Science - Dining Philosophers Problem.
Пять философов сидят за круглым столом и пьют...виноградный сок наверно. Между ними лежат пять вилок...серебряных. Чтобы поесть, каждому нужны две вилки: левая и правая. Начинается драка за вилки? Нет.
В Go это удобно моделировать через горутины и мьютекс.
Но есть проблема: если несколько горутин одновременно захватят по одной вилке и будут ждать вторую, программа может зависнуть.
Это называется deadlock.
1. Почему wg.Wait() не виноват? Представим такой код: var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { defer wg.Done() eat() }() } wg.Wait()
Иногда программа падает с ошибкой: fatal error: all goroutines are asleep - deadlock! И кажется, что проблема в WaitGroup.
Но wg.Wait() может быть лишь местом, где runtime обнаруживает проблему. Главный горутина ждёт завершения остальных, а остальные уже заблокировались где-то внутри eat().
2. Deadlock может быть не только в mutex В исходном примере с пятью философами была ещё одна проблема. Использовались два небуферизованных канала: permissionChannel := make(chan bool) finishEating := make(chan bool) Философ отправлял сообщение о завершении еды: finishEating <- true А отдельная goroutine должна была его принять. Но она могла в этот момент сама заблокироваться на отправке разрешения: permissionChannel <- true Получалась цепочка: философ ждёт, пока примут finishEating; управляющая goroutine ждёт, пока философ примет permissionChannel; никто не может продолжить работу. Классический deadlock через каналы. Именно поэтому поиск проблемы только в мьютеск не помог бы.
3. Как диагностировать? Первое, что стоит сделать, - посмотреть полный stack trace при deadlock. Go показывает, на каких операциях заблокированы goroutines. Например: goroutine 1 [semacquire]: sync.(*WaitGroup).Wait(...) goroutine 7 [chan send]: main.eat(...) goroutine 8 [chan send]: main.permissionFromHost(...)
Это уже гораздо полезнее, чем просто видеть строку wg.Wait(). Также можно запустить: go vet ./...
В исходном примере go vet обнаруживает ещё одну ошибку: sync.Mutex передавался по значению.
Mutex нельзя копировать после начала использования. Его нужно передавать по указателю: func eat(left, right *sync.Mutex) { left.Lock() defer left.Unlock() right.Lock() defer right.Unlock() // Eating }
Это отдельная ошибка, не объясняющая сама по себе deadlock в каналах.
Что важно запомнить Deadlock не обязательно возникает там, где программа перестала отвечать. wg.Wait() может быть последним местом, до которого дошёл главный goroutine, но настоящая причина может находиться в совершенно другой части программы.
Когда видишь: fatal error: all goroutines are asleep - deadlock! не стоит сразу подозревать WaitGroup.
Посмотри стактрейс и выясни: кто ждёт mutex; кто отправляет данные в канал; кто должен эти данные прочитать; есть ли вообще горутин, которая может выполнить ожидаемую операцию.
В конкурентном коде важно не только то, кто чего ждёт, но и кто должен разблокировать ожидающего.
Время пошло!
· вчера
Хорошо, что отдельно сказано про wg.Wait(): многие ищут проблему там, где runtime просто её обнаружил. Про небуферизованные каналы тоже полезно, там блокировка не такая очевидная, как с мьютексами. Для поиска таких мест вы больше полагаетесь на дамп горутин или на race detector?
0
ответить
коммент скрыт — часть юзеров считает его токсичным или некорректным
коммент удалён