Как не надо использовать sync pool в go.

AlexG
3 min readSep 6, 2020

--

Эта заметка о пользовательском опыте и не является руководством по использованию. Тут нет технических подробностей о внутреннем устройстве или производительности sync pool.

Если в работе вы используетет язык go, то у вас может возникнуть желание поэкспериментировать с использованием sync pool. Однажды такая возможность предоставилась и мне. Сделав задачу и написав тест, я получил ошибку, разобравшись с ней, решил написать эту заметку.
С чего я вдруг решил, что один баг заслуживает столько внимания? Потому что чтение этой заметки занимает 5 минут, а потраченное время на поиск ошибки и нервы бесценны :) + у меня не нашлось знакомых с “позитивным опытом работы” с пулом, зато нашлось много тех, кто хотел бы поэкспериментировать с ним + поиск решения проблемы в интернете не дал особых результатов — кто же будет писать про маленький баг, когда можно написать про внутренности или производительность пула? Стоит заметить, что в процессе работы с пулом я просмотрел много статей, которые меня больше запутали, нежели внесли ясность. В документации сказано: пул — это набор временных объектов, которые могут быть индивидуально сохранены и извлечены. Т е вместо того, чтобы выделять память под обьект и оставлять ее сборщику мусора, после того как она была использована, мы можем складывать эти отработанные кусочки памяти в пул, а затем доставать их и использовать при необходимости для уменьшения количества аллокаций и снижения нагрузки на сборщик мусора. Для использования пула следует задать функцию получения новых обьектов, а также у него есть всего два метода — Put и Get. Кажется, что все просто и тут негде ошибиться…

Итак, чтобы понять как пул себя ведет, создадим пул и рассмотрим 3 абстрактных, упрощенных сценария (код на гитхаб), которые мы будем запускать в паралельно работающих горутинах:

type PoolStorage struct {
sp *sync.Pool
}
func New() *PoolStorage {
sp := &sync.Pool{
New: func() interface{} { return &A{} },
}
return &PoolStorage{
sp: sp,
}
}

func (p *PoolStorage) Put(s *A) {
p.sp.Put(s)
}

func (p *PoolStorage) Get() *A {
s := p.sp.Get().(*A)
return s
}

1. Код первого сценария:

func (p *PoolStorage) sc1() {
a := &A{}
a.Str = "test"
p.Put(a)
time.Sleep(time.Millisecond) // for race detector newA := p.Get()
newA.Str = "change"
if a.Str == "change" {
log.Println("str field changed")
}
}

текущая или другая горутина изменяет исходный обьект после взятия его из пула.

2. Код второго сценария:

func (p *PoolStorage) sc2() {
a := p.Get()
r := rand.Int()
a.Int = r
p.Put(a)
time.Sleep(time.Millisecond) // for race detector if a.Int != r {
log.Println("int field changed")
}
}

другая горутина изменит значение поля в обьекте.

3. Код третего сценария:

func (p *PoolStorage) sc3() {
a := p.Get()
if a.Str == "old" {
log.Println("no zero value") // its ok
}
time.Sleep(time.Millisecond) a.Str = "old"
p.Put(a)
}

с большой вероятностью можно достать из пула отработавший ранее обьект с не нулевым значением.

В 1-ом и 2-ом примере можно обнаружить ошибку, используя детектор гонки (go test . -bench=Benchmark_SC1 --race), только надо добавить небольшую временную задержку. В 3-ем примере такой ошибки нет, это нормальный вариант, который как бы говорит нам: “что положишь - то и возмешь”.
Итак, сами ошибки 1-го и 2-го сценария происходят из неправильной логики работы с пулом — в коде текущей горутины нельзя обращаться к обьекту, который был положен в пул, т. к. в этот момент его уже взяла в работу другая горутина и, начав с ним работать, обеспечивает состояние гонки. Эта простая мысль, которой я хотел поделиться. Надеюсь она сэкономит время тем, кто только начал работать с пулом. Если говорить о конкретном применении в моей задаче, то пул давал незначитальны выйгрыш в 5%, поэтому был отложен до лучших времен. На этом стоит завершить рассказ. Спасибо за внимание.

Тут я оставлю пару ссылок, которые мне понравились:
https://www.akshaydeo.com/blog/2017/12/23/How-did-I-improve-latency-by-700-percent-using-syncPool/
http://dominik.honnef.co/go-tip/2014-01-10/#syncpool

--

--