Не могу понять эту ситуацию тупиковой блокировки в Голанге, мне нужно ниже перейти к коду с шаблоном pub и sub.
package main
import (
"fmt"
"sync"
)
func main() {
cond := sync.NewCond(&sync.Mutex{})
subscribe := func(c *sync.Cond, fn func()) {
var goroutineRunning sync.WaitGroup
goroutineRunning.Add(1)
go func() {
goroutineRunning.Done()
fmt.Println("waiting")
c.L.Lock()
c.Wait()
c.L.Unlock()
fn()
}()
goroutineRunning.Wait()
}
var clickRegistered sync.WaitGroup
clickRegistered.Add(2)
subscribe(cond, func() {
fmt.Println("notified 1")
clickRegistered.Done()
})
subscribe(cond, func() {
fmt.Println("notified 2")
clickRegistered.Done()
})
cond.Broadcast()
clickRegistered.Wait()
}
Я не мог понять, почему у меня возникает тупиковая блокировка, если я ставлю fmt.Println("waiting")
после goroutineRunning.Done()
, если я удаляю fmt.Println("waiting")
или перемещаюсь выше goroutineRunning.Done()
, все работает так, как ожидалось, почему это происходит так?
Нужно запустить go run main.go несколько раз, чтобы получить мертвую блокировку, это может сработать с первого раза.
В вашем коде есть несколько возможных ситуаций, когда cond.Broadcast()
вызывается:
c.Wait()
и освобождаются.c.Wait()
.cond.Broadcast()
пробуждает все горутины, ожидающие cond
, но, как указано выше, возможно, что одна (или обе) горутины еще не достигли этой точки в своем выполнении (поэтому не ждут cond
и не будут выпущены). Если это произойдет, они будут заблокированы навсегда по адресу c.Wait()
, поскольку дальнейших вызовов cond.Broadcast()
не будет.
Итак, мы имеем здесь гонку; если обе горутины достигнут c.Wait()
до того, как основная горутина достигнет cond.Broadcast()
, программа завершится. Если что-то не получается, программа блокируется (основная горутина заблокирована в clickRegistered.Wait()
и 1 или 2 горутины заблокированы в c.Wait()
). В результате поведение программы непредсказуемо и зависит от таких факторов, как ваша ОС, версия Go, процессор. архитектура, текущая нагрузка и т. д.
Есть некоторые вещи, которые вы можете сделать, чтобы повлиять на поведение. Например, добавление вызова runtime.GOMAXPROCS(1) в начале вашей программы, вероятно (это не гарантировано!) будет означать ее завершение (даже с fmt.Println
). Это связано с тем, что при работе с одним процессором планировщик Go, скорее всего, позволит каждой горутине запускаться c.Wait()
(это не гарантировано; планировщик может упреждать выполнение - также). При наличии нескольких ядер ЦП горутины (вероятно) будут работать на нескольких ядрах, поэтому ситуация становится менее предсказуемой.
Добавление кода выше goroutineRunning.Done()
не окажет никакого влияния, поскольку основная горутина не может выйти за пределы goroutineRunning.Wait()
до того, как горутины вызовут goroutineRunning.Done()
. Однако любая задержка (например, вызов fmt.Println("waiting")
) после этой точки увеличивает вероятность того, что горутина не достигнет c.Wait()
до того, как основная горутина достигнет cond.Broadcast()
(что приведет к тупику).
Спасибо @Brits за подробное объяснение