3.7Kпросмотров
29 июля 2025 г.
📷 ФотоScore: 4.0K
🤪 Surprising WaitGroup Panic Часто ли вы ловили паники, используя WaitGroup? - не думаем. Сегодняшняя история об одной из таких, зарытой где-то в недрах, вас очень удивит Не будем томить, приступаем! Для начала рассмотрим избитую структуру:
type WaitGroup struct { noCopy noCopy state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count. sema uint32
}
— noCopy - специфическая структура, используемая утилитой govet для маркировки откопированных значений
— sema - используется внутренними функциями пакета sync, чтобы реализовать блокировку и разблокировку
— state - отвечает за счетчик и количество ожидающих горутин Углубимся в рассмотрение поля state:
— Старшие 32 бита представляют из себя счетчик, который инкрементируется при вызовах Add(N) и декрементируется при вызовах Done()
— Младшие 32 бита являются количеством ожидающих потоков Теперь посмотрим на внутреннюю логику метода Add: func (wg *WaitGroup) Add(delta int) { // ... state := wg.state.Add(uint64(delta) << 32) v := int32(state >> 32) w := uint32(state) // ... if v < 0 { panic("sync: negative WaitGroup counter") } // ... }
— Прибавляем к счетчику наш delta, сдвигая его влево на 32 бита
— Извлекаем значение счетчика из state, сдвигая его вправо на 32 бита
— Извлекаем количество ожидающих потоков из state, что нас не особо интересует :) Ключевое здесь то, что инкрементация происходит до всех проверок Смотря на этот код, мы задумались о том, а что если наш counter окажется переполненным? Ответ не заставил себя долго ждать. И вот почему! Семантически счетчик воспринимается как int32, то есть один старший бит отвечает за знак, а остальные за мантиссу (само число). В случае, когда счетчик был инкрементирован методом Add(N) до состояния, когда все младшие биты установлены в 1 (счетчик имеет значение 2_147_483_647) и к этому значению мы прибавляем 1, то счетчик становится переполненным и вместо ожидаемого значения 2_147_483_648, мы получаем значение -2_147_483_648 Вот, что происходит на битовом уровне:
До Add(1):
Двоичное: 01111111 11111111 11111111 11111111
int32: 2147483647 После Add(1):
Двоичное: 10000000 00000000 00000000 00000000
int32: -2147483648 Итого: переменная v в методе Add будет иметь отрицательное значение, что вызовет панику Для воспроизведения этого сценария вы можете воспользоваться следующими двумя сниппетами
var wg sync.WaitGroup wg.Add(math.MaxInt32)
// goroutines launch... wg.Add(1) // panic: sync: negative WaitGroup counter
// goroutine launch... var wg sync.WaitGroup
for range math.MaxInt32 + 1 { wg.Add(1) // panic: sync: negative WaitGroup counter // goroutines launch...
} Мораль
WaitGroup не счетная палата и не готова к отслеживанию популяции бактерий на Венере. Пристально следите за своими Add'ами и Done'ами, не отпуская их на волю, и контролируйте количество потоков в вашей системе Статью писали с Дашей: @dariasroom Stay tuned ❤️
#golang #concurrency