Кодосмотр: Делитесь памятью, общаясь

Pop Out Code
code on leftright code width 70% filepaths shownhidden
Введение
Подход Go к параллелизму отличается от традиционного использования потоков и общей памяти. Философски его можно кратко выразить следующим образом:

Не общайтесь, разделяя память; разделяйте память, общаясь.

Каналы позволяют передавать ссылки на структуры данных между горутинами. Если рассматривать это как передачу владения данными (возможность их читать и записывать), то они становятся мощным и выразительным механизмом синхронизации.

В этом кодосмотре мы рассмотрим простую программу, которая опрашивает список URL-адресов, проверяет их HTTP-коды ответа и периодически выводит их состояние.
doc/codewalk/urlpoll.go
Тип State
Тип State представляет состояние URL.

Pollers отправляют значения State в StateMonitor, который поддерживает карту текущего состояния каждого URL.
doc/codewalk/urlpoll.go:26,30
Тип Resource
Resource представляет состояние URL, который нужно опрашивать: сам URL и количество ошибок, возникших с момента последнего успешного опроса.
doc/codewalk/urlpoll.go:60,64


Когда программа запускается, она выделяет один Resource для каждого URL. Главный goroutine и goroutines Poller обмениваются Resource через каналы.
doc/codewalk/urlpoll.go:60,64
Функция Poller
Каждый Poller получает указатели на Resource из входного канала. В этой программе принято, что отправка указателя Resource по каналу передаёт владение данными от отправителя получателю. Благодаря этому соглашению мы знаем, что никакие два goroutine не будут одновременно обращаться к одному и тому же Resource. Это означает, что нам не нужно заботиться о блокировках для предотвращения одновременного доступа к этим структурам данных.

Poller обрабатывает Resource, вызывая его метод Poll.

Он отправляет значение State в канал статуса, чтобы сообщить StateMonitor о результате Poll.

Наконец, он отправляет указатель Resource в канал out. Это можно интерпретировать как то, что Poller говорит: «Я закончил с этим Resource» и возвращает владение им обратно главному goroutine.

Несколько goroutine запускают Poller'ы, обрабатывая Resource параллельно.
doc/codewalk/urlpoll.go:86,92
Метод Poll
Метод Poll (типа Resource) выполняет HTTP HEAD-запрос для URL Resource и возвращает код статуса HTTP-ответа. Если происходит ошибка, Poll выводит сообщение в стандартный поток ошибок и возвращает строку ошибки.
doc/codewalk/urlpoll.go:66,77
Функция main
Функция main запускает goroutine Poller и StateMonitor, а затем циклически передаёт завершённые Resource обратно в канал pending после соответствующих задержек.
doc/codewalk/urlpoll.go:94,116
Создание каналов
Сначала main создаёт два канала типа *Resource, pending и complete.

Внутри функции main новый goroutine отправляет один Resource на каждый URL в канал pending, а главный goroutine получает завершённые Resource из канала complete.
doc/codewalk/urlpoll.go:95,96


Ожидающие и завершившиеся каналы передаются каждой из горутин Poller, внутри которых они известны как in и out.
doc/codewalk/urlpoll.go:95,96
Инициализация StateMonitor
StateMonitor инициализирует и запускает горутину, которая хранит состояние каждого Resource. Эта функция будет рассмотрена подробно позже.

Пока что важно отметить, что она возвращает канал типа State, который сохраняется как status и передаётся горутинам Poller.
doc/codewalk/urlpoll.go:98,99
Запуск горутин Poller
Теперь, когда у функции main есть необходимые каналы, она запускает несколько горутин Poller, передавая каналы в качестве аргументов. Каналы обеспечивают средства связи между главной горутиной, горутинами Poller и StateMonitor.
doc/codewalk/urlpoll.go:101,104
Отправка Resource в pending
Чтобы добавить начальную работу в систему, main запускает новую горутину, которая выделяет и отправляет один Resource на каждый URL в канал pending.

Новая горутина необходима, потому что отправка и получение по не буферизованным каналам являются синхронными. Это означает, что такие отправки блокируются до тех пор, пока Poller не будет готов читать из pending.

Если бы эти отправки выполнялись в главной горутине с меньшим количеством Poller, чем отправок в канал, программа бы попала в состояние взаимной блокировки, поскольку main ещё не начала получать из complete.

Упражнение для читателя: измените эту часть программы так, чтобы она читала список URL из файла. (Можно переместить эту горутину в отдельную именованную функцию.)
doc/codewalk/urlpoll.go:106,111
Главный цикл событий
Когда Poller завершает работу с Resource, он отправляет его в канал complete. Этот цикл получает указатели на Resource из complete. Для каждого полученного Resource запускается новая горутина, вызывающая метод Sleep у Resource. Использование отдельной горутины для каждого Resource гарантирует, что сны могут происходить параллельно.
doc/codewalk/urlpoll.go:113,115


Обратите внимание, что любой единственный указатель на Resource может быть отправлен либо в pending, либо в complete только в один момент времени. Это гарантирует, что Resource либо обрабатывается горутиной Poller, либо находится в режиме сна, но никогда одновременно в обоих состояниях. Таким образом, мы разделяем данные Resource через коммуникацию.
doc/codewalk/urlpoll.go:113,115
Метод Sleep
Sleep вызывает time.Sleep для паузы перед отправкой Resource в done. Пауза будет либо фиксированной длины (pollInterval), плюс дополнительная задержка, пропорциональная количеству последовательных ошибок (r.errCount).

Это пример типичного идиомы Go: функция, предназначенная для выполнения внутри горутины, принимает канал, на который она отправляет своё возвращаемое значение (или другое указание о завершённом состоянии).
doc/codewalk/urlpoll.go:79,84
StateMonitor
StateMonitor получает значения State по каналу и периодически выводит состояние всех Resource, которые обрабатываются программой.
doc/codewalk/urlpoll.go:32,50
Канал updates
Переменная updates — это канал типа State, на который горутины Poller отправляют значения State.

Этот канал возвращается функцией.
doc/codewalk/urlpoll.go:36
Карта urlStatus
Переменная urlStatus — это карта URL-адресов и их последних статусов.
doc/codewalk/urlpoll.go:37
Объект Ticker
time.Ticker — это объект, который повторно отправляет значение по каналу с заданным интервалом.

В данном случае ticker вызывает печать текущего состояния в стандартный вывод каждые updateInterval наносекунд.
doc/codewalk/urlpoll.go:38
Горутина StateMonitor
StateMonitor будет работать вечно, выполняя выборку из двух каналов: ticker.C и update. Инструкция select блокируется до тех пор, пока одно из её сообщений не будет готово к выполнению.

Когда StateMonitor получает сигнал от ticker.C, он вызывает функцию logState для вывода текущего состояния. Когда же он получает обновление состояния от updates, он записывает новое состояние в карту urlStatus.

Обратите внимание, что эта горутина владеет структурой данных urlStatus, обеспечивая тем самым последовательный доступ к ней. Это предотвращает проблемы с повреждением памяти, которые могут возникнуть при параллельном чтении и/или записи общей карты.
doc/codewalk/urlpoll.go:39,48
Заключение
В этом руководстве мы рассмотрели простой пример использования примитивов параллелизма Go для совместного использования памяти через коммуникацию.

Это должно стать отправной точкой для изучения различных способов, в которых горутины и каналы могут быть использованы для написания выразительных и лаконичных конкурентных программ.
doc/codewalk/urlpoll.go
GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang