Конкурентность — Примеры

Пример 1 — Пул, который всегда завершается

Задание импорта проходит по списку заказов и на каждом вызывает медленный поиск, а первая версия заводит горутину на заказ и возвращается сразу после обхода списка, поэтому процесс успевает завершиться на половине работы. Нужна версия с фиксированным числом работников, которая возвращается только тогда, когда обработан последний заказ.

for _, o := range orders {
	go enrich(o)          // никто их не ждёт
}
return nil

Решение

jobs := make(chan Order)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for o := range jobs {
			enrich(o)
		}
	}()
}
for _, o := range orders {
	jobs <- o
}
close(jobs)               // закрывает отправитель, никогда приёмник
wg.Wait()

О том, что заказов больше не будет, каждому работнику сообщает закрытие канала. О завершении работы сообщает группа ожидания, поэтому функция возвращается только после последнего заказа. В первой версии на каждый заказ заводится своя горутина, нигде не учтённая, и каждая незавершённая держит свой стек. А канал, закрытый приёмником, вызывает панику на следующей отправке — именно на этом обычно и ошибаются.

Пример 2 — Кто ждёт, когда очередь заполняется

Читатель производит события быстрее, чем писатель успевает их сохранять; буфер добавили, чтобы сгладить всплески, а память растёт, пока процесс не убьют. Выберите буфер и политику на случай заполнения и напишите версию, которая их применяет.

var events []Event
for e := range source {
	events = append(events, e)   // растёт без ограничения
	write(e)
}

Решение

// 32 события поглощаются без ожидания, затем производитель замедляется.
queue := make(chan Event, 32)
go writeAll(queue)

for e := range source {
	queue <- e          // при заполнении эта отправка ждёт
}
close(queue)

Буфер указанного размера разъединяет прибытие и обработку до этого размера, а после него отправка ждёт и читатель удерживается на скорости писателя; это осознанный выбор вместо отбрасывания событий. Однако неограниченный срез — не буфер побольше, а рост памяти без потолка. Обычно ошибаются иначе: размер буфера подбирают, чтобы спрятать всплеск нагрузки, вместо того чтобы решить и записать, что происходит при заполнении.