вот код //go:build !solution package batcher imp...
Created on: April 16, 2025
Created on: April 16, 2025
вот код
//go:build !solution
package batcher
import (
"gitlab.com/slon/shad-go/batcher/slow"
"sync"
"sync/atomic"
)
type Batcher struct {
atomicVar *slow.Value
value any
process atomic.Bool
m sync.RWMutex
wg sync.WaitGroup
ch chan struct{}
}
func NewBatcher(v *slow.Value) *Batcher {
var b Batcher
b.process.Store(false)
b.atomicVar = v
b.m = sync.RWMutex{}
b.wg = sync.WaitGroup{}
b.ch = make(chan struct{}, 2)
b.ch<-struct{}{}
b.ch<-struct{}{}
return &b
}
func (b *Batcher) Load() any {
if !b.process.Swap(true) {
<-b.ch
<-b.ch
b.wg.Add(1)
go func() {
newVal := b.atomicVar.Load()
b.process.Store(false)
textb.m.Lock() b.value = newVal b.m.Unlock() b.wg.Done() }() b.wg.Wait() b.ch<-struct{}{} } b.wg.Wait() b.ch<-struct{}{} b.m.RLock() defer b.m.RUnlock() return b.value
}
вот такой тест блокируется
func TestTwoParallelLoads(t *testing.T) {
defer goleak.VerifyNone(t)
var value slow.Value
b := NewBatcher(&value)
textvalue.Store(1) go func() { require.Equal(t, 1, b.Load()) }() require.Equal(t, 1, b.Load())
}
помоги найти, при каком исполнении теста происходит бокировка
Наблюдаемый «зависон» (deadlock) появляется в том случае, если обе горутины вызывают Load примерно одновременно и обе «успевают» увидеть b.process == false до того, как хоть одна из них успела сделать Swap(true). В результате обе одновременно заходят в ветку if !b.process.Swap(true), и каждая пытается вынуть из канала по два токена, всего получается 4 receive на канал, который изначально содержит только 2 элемента. Это и приводит к блокировке.
Ниже — подробнее, что происходит пошагово при неудачном планировании:
Начальные условия:
b.process = falseb.ch имеет емкость 2 и уже содержит 2 токена.b.Load() почти одновременно.Обгон (race) на process.Swap(true):
if !b.process.Swap(true) и видит, что process = false, поэтому !Swap(true) == true, она заходит в блок if.Swap(true), G2 тоже заходит на ту же проверку. Ей тоже «везет» увидеть process = false.Поскольку вызов Swap(true) атомарный, каждая горутина действительно «забирает» старое значение — но важно, что проверка if !b.process.Swap(true) (то есть чтение «было ли false?») может успеть пройти у обеих до того, как одна из них завершит операцию. На уровне кода это выглядит так:
go// Горутинa1: old := b.process.Swap(true) if !old { // old == false => пошла внутрь ... } // Горутинa2: old := b.process.Swap(true) if !old { // old тоже == false => пошла внутрь ... }
При крайне неудачном планировании обе видят старое значение false.
Обе горутины заходят в ветку и пытаются вычитать 2 токена из канала:
<-b.ch, <-b.ch. Канал был размером 2 и как раз содержал 2 токена, так что эти операции прошли успешно, канал опустел.receive, G2 тоже начала их делать (поскольку планировщик может «переключиться» между ними где угодно), G2 тоже хочет сделать свои 2 receive. В итоге они пытаются получить в сумме 4 «штуки» из канала, в котором всего 2.Как только канал исчерпывается, оставшиеся receive блокируются навсегда, поскольку новых «вкладываний» (send) в него до этого момента никто не сделал.
Итог: обе горутины оказываются повисшими на receive из канала, потому что ни одна не сделала b.ch<-struct{}{} обратно.
Таким образом, чтобы «спровоцировать» такой deadlock, нужно одновременно (или почти одновременно) вызвать два Load() при b.process == false. То есть планировщик должен так распределить процессорное время, что обе горутины успеют пройти проверку if !b.process.Swap(true) (и считать оттуда false), прежде чем одна из них выполнит Swap(true) до конца. Именно тогда получается ситуация, в которой обе конкурирующие горутины пытаются вынуть 2 токена каждая из общего канала с двумя токенами, и тест «зависает».
разве атомарность Swap не гарантирует, что такое не произойдет
Короткий ответ:
Атомарность Swap действительно гарантирует, что при одновременном вызове b.process.Swap(true) две горутины не смогут увидеть старое значение false в один и тот же момент. Однако в данном коде есть логика, которая позже снова выставляет b.process = false (через Store(false) в горутине). Поэтому первая горутина может сделать Swap(true) → получить false и внутри себя (в анонимной горутине) после некоторой работы вызвать Store(false). А вторая горутина, зайдя чуть позже, снова обнаружит b.process == false и тоже войдёт в if !b.process.Swap(true). И тогда обе начнут забирать элементы из канала по два раза, что приводит к потенциальной блокировке.
Ниже более детально:
Swap не даёт двум горутинам одновременно увидеть falseАтомарная операция Swap(true) (Go предоставляет её через atomic.Value или atomic.Bool / CompareAndSwap, и т.п.) гарантирует, что она:
b.process на true.Причём эти действия не «разрываются» по времени и не могут выполняться одновременно двумя горутинами так, чтобы обе увидели одно и то же старое значение. Если горутина G1 сделала old := Swap(true), то когда горутина G2 дойдёт до Swap(true), она уже прочитает обновлённое значение, а не старое.
Таким образом, если бы код выглядел так:
goif !b.process.Swap(true) { // ... что-то делаем один раз }
и нигде больше в параллели не было бы b.process.Store(false), то только одна горутина смогла бы зайти внутрь if. Первая «успевшая» горутина увидит старое значение false, а вторая — уже старое значение true.
В вашем фрагменте кода внутри Load() есть анонимная горутина, которая после «медленной» загрузки данных делает:
gob.process.Store(false)
Другими словами, вот упрощённая схема:
if !b.process.Swap(true) { … } и видит там false (старое значение).b.process.Store(false).b.process уже снова стал false.Load() и снова делает if !b.process.Swap(true). Так как b.process = false, Swap(true) вернёт false. Значит, G2 тоже зайдёт в блок if.Таким образом, не нарушается атомарность Swap, а просто код повторно возвращает флаг b.process в false, давая второй (и третьей, и любой) горутине шанс снова «увидеть» внутри if !Swap(true) старое значение false.
Посмотрим ещё раз логику внутри if !b.process.Swap(true) {...}:
goif !b.process.Swap(true) { <-b.ch <-b.ch b.wg.Add(1) go func() { newVal := b.atomicVar.Load() b.process.Store(false) b.m.Lock() b.value = newVal b.m.Unlock() b.wg.Done() }() b.wg.Wait() b.ch <- struct{}{} } b.wg.Wait() b.ch <- struct{}{}
В начале блока мы забираем из канала два токена: <-b.ch, <-b.ch.
Канал b.ch был создан размером 2, и при инициализации туда положили два struct{}{}:
gob.ch = make(chan struct{}, 2) b.ch <- struct{}{} b.ch <- struct{}{}
После того как внутренняя анонимная горутина отработала, один токен (точнее b.ch<-struct{}{}) возвращается в канал, потом (уже после b.wg.Wait()) ещё один. Итого, чтобы корректно «освободить» канал обратно на 2 токена, нужно пройти всю эту цепочку.
Если две горутины (G1 и G2) почти одновременно (или последовательно, но пока первая полностью не восстановила токены в канал) заходят в ветку if ..., каждая будет стараться забрать по 2 токена. Итого — 4 попытки receive (<-b.ch) против имеющихся 2 буферных слотов в канале. В итоге вторая горутина «повисает» в попытке получить «лишние» токены, которых просто нет. Так получается дедлок.
Swap не означает «каждая горутина всегда увидит разное значение, если другой поток что-то делает»: она лишь гарантирует, что в рамках одного вызова Swap(true) возвращаемое «старое» значение корректно и изменяется атомарно.b.process специально сбрасывается обратно в false внутри анонимной горутины, что даёт второй горутине возможность снова «зайти» в блок if !b.process.Swap(true).<-b.ch два раза) и только после долгой последовательности действий мы кладём токены назад, одновременно (или почти одновременно) запущенные горутины могут «вычитать» из канала в сумме больше, чем туда возвращается в нужный момент, — и «застрять».Другими словами, «атомарность» убирает гонки по чтению/записи одного флага, но сама по себе не решает проблему того, когда этот флаг снова выставляется в false и как синхронизируются операции с каналом. Именно это «повторное обнуление» и канал с лимитом «2» создают возможность для дедлока.
вернемся к причине deadlock. Опиши подробнее исполнение, при котором происходит deadlock
вернемся к причине deadlock. Опиши подробнее исполнение, при котором происходит deadlock