Просмотрел видео "Google I/O 2013 - Advanced Go Concurrency Patterns"
https://www.youtube.com/watch?v=QDDwwePbDtwЧто стоит запомнить и применить?
1. Техника "цикл for-select"
2. Техника "Служебный канал, канал для ответа (chan chan error)"
3. Техника "nil-каналы в выражениях select для временной приостановки"
4. Используйте инструменты для обнаружения состояния гонки данных (data race, флаг -race) и взаимоблокировок.
На видео описывется три ошбики к коде с конкурентным исполнением (
1 - состояние гонки при обращении к данным /
решение (канал с каналом для отчетов/ошибок),
2-приостановка цикла из-за Sleep /
Решение (делаем задержку по готовности канала),
3-цикл может заблокироваться навсегда если нет данных в канале) и способы их решения приведенными техниками.
Также даны несколько техник улучшения работы главного цикла обработки:
- Дедупликация заданий для дочерних элементов перед их исполнением в горутине (код на видео, фильтрация через map уже обработанных ранее заданий)
- Как приостановить выборку заданий, если их уже слишком много в очереди (код в видео, использет технику 3)
- Как сделать основную процедуру получения данных неблокируемой (код в видео, создает канал для результатов, техника 2)
1. Техника "цикл for-select"
Позволяет избежать блокировки цикла в одном из состояний, все происходи внутри одной горутины. Пример кода (
на видео):
func(s *sub) loop() {
... определяем изменеяемое состояние ...
for {
... задаем канале для разных случаев ...
select {
case <-c1: // прочитать из канала без сохранения в переменную
... прочитать/записать состояние ...
case с2 <- x: // записать в канал
... прочитать/записать состояние ...
case y := <-c3: // прочитать из канала в переменную
... прочитать/записать состояние ...
}
}
}
2. Техника "Служебный канал, канал для ответа (chan chan error)"
Когда мы используем горутины проверка завершения выполнения по булеву флагу может
привести к гонке данных (пример на видео). Это может определить детектор состояний гонки, запустите go build -race main.go . Чтобы этого избежать создадим канал передающий канал. Пример кода (
на видео):
type sub struct {
closing chan chan error // запрос ответ
}
// использование
func (s *sub) Close() {
errChan = make(chan error)
s.closing <- errChan
return <-errChan
}
// Обработка сигнала закрытия в loop
func (s *sub) loop() {
...
var err error // задается когда в произошла ошибка во время выполнения основной работы
for {
select {
case errChan := <-s.closing: // проверяем есть ли сигнал на завершение работы
errChan <- err // вернем ошибку через предоставленный канал, может быть nil или объект error
close(s.updates) // закрываем канал пересылки для данных в основную горутину
return // завершим работу loop()
}
}
...
}
Выглядит достаточно странно, канал с каналом ошибок. Эта конструкция позволяет сделать двунаправленный обмен между горутинами. Мы передаем канал через который vs можем вернуть ответ. Метод loop() это как небольшой сервер, и чтобы его остановить мы даем ему запрос на прекращение работы - пишем значение в канал sub.closing, в канал мы передаем канал в который сервер поместит ответ, когда закончит работу. В случае штатной остановки в канале будет nil, иначе канал вернет error.
3. Техника "nil-каналы в выражениях select для временной приостановки"
Если мы хотим отправлять в канал элементы по одному, т.е. по готовности мы можем выбирвать их из очереди ожидающих обработки в момент её завершения. Пример кода:
var pending []Item // заполняется процедурой получения данных, очищается процедурой отправки отчетов о работе
// отправляем информацию о завершенной задаче в канал s.updates
for {
select {
case s.updates <- pending[0]: // отправляем первый элемент
pending = pending[1:] // когда отправка удаласть, удаляем первый элемент из массива перерписваивая слайс без него
}
}
// это будет падать с ошибкой
Почему это код завершаться с ошибкой? В тот момент когда pending становиться пустым мы не можем обрататиться к его первому элементу.
У каналов есть такая особенность, если каналу присвоить значение nil, то отправка и прием блокируется в этом канале. Мы его деинициализируем вручную.
Эту особенность можно применять для введения верменной блокировки.
Пример кода:
a := make(chan string)
go func(){ a <- "a" }()
a = nil
select {
case s:=<-a: // тут канал будет заблокирован
fmt.Println("s=", s)
}
Так мы можем временно отключать некоторые варианты исполнения в select.
Итак исправим проблему этим методом:
var pending []Item // заполняется процедурой получения данных, очищается процедурой отправки отчетов о работе
// отправляем информацию о завершенной задаче в канал s.updates
for {
var first Item
var updates chan Item
if len(pending) > 0 {
first = pending[0]
updatesChan = s.updates // укажем реальный канал, чтобы разблокировать исполнение в select
}
select {
case updatesChan <- first: // отправляем первый элемент, заблокируется, если канал s.updates = nil
pending = pending[1:] // когда отправка удаласть, удаляем первый элемент из массива перерписваивая слайс без него
}
}