GoLang: продвинутые техники для конкурентного программирования

Sep 24, 2019 16:58

Просмотрел видео "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-цикл может заблокироваться навсегда если нет данных в канале) и способы их решения приведенными техниками.

Также даны несколько техник улучшения работы главного цикла обработки:
  1. Дедупликация заданий для дочерних элементов перед их исполнением в горутине (код на видео, фильтрация через map уже обработанных ранее заданий)
  2. Как приостановить выборку заданий, если их уже слишком много в очереди (код в видео, использет технику 3)
  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:] // когда отправка удаласть, удаляем первый элемент из массива перерписваивая слайс без него
  }
}

конкурентное программирование, черновик, технические заметки, golang, программирование, работа

Previous post Next post
Up