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; кто отправляет данные в канал; кто должен эти данные прочитать; есть ли вообще горутин, которая может выполнить ожидаемую операцию.

В конкурентном коде важно не только то, кто чего ждёт, но и кто должен разблокировать ожидающего.

Время пошло!