Назад к блогу

Каналы в Go: как горутины общаются друг с другом

Каналы в Go: как горутины общаются друг с другом

Общая память и блокировки

В классической модели несколько потоков работают с одними и теми же данными. Например, берут задания из общего списка и отмечают те, что уже обрабатываются. Чтобы два потока не полезли менять список одновременно, доступ защищают блокировкой.

Но эта защита не бесплатна. Потоки начинают конкурировать за блокировку, а в коде нужно внимательно следить, где её захватить и где освободить. В Python для безопасного обмена готовыми структурами есть Queue: она сама синхронизирует доступ. В исходной статье похожую идею сопоставляют с передачей данных через каналы: вместо того чтобы вместе менять состояние, участники передают друг другу работу.

Принцип обмена данными в Go

Кто отвечает за состояние, пока задача выполняется? В Go ответом часто становится не mutex, а канал: goroutine получает значение, обрабатывает его и отправляет дальше.

В примере с проверкой URL Poller читает ресурсы из входного канала, проверяет их и передаёт результат в выходной. Здесь goroutine — независимо выполняющаяся часть программы, а channel связывает её с остальными: через него поступают данные и возвращаются обработанные. Структуре Resource больше не нужны поля, которые отмечают занятость и время последней проверки.

Принцип может поначалу звучать непривычно: не обменивайтесь данными, разделяя память; разделяйте память, обмениваясь данными. На практике это просто значит: вместо совместного изменения одного объекта goroutine передаёт его другой и больше не трогает, пока объект у получателя. Так в конкретный момент у данных есть один ответственный владелец, а координация становится частью потока сообщений.

В исходной статье этот подход показан на обработке списка URL: канал передаёт ресурс обработчику, а затем принимает его обратно.

Poller с каналами

Poller не ищет ресурс в общем списке и не меняет его состояние по дороге. Он получает указатель на Resource из in, проверяет URL и отправляет тот же ресурс в out:

type Resource string

func Poller(in, out chan *Resource) {
    for r := range in {
        // Проверка URL из r

        out <- r
    }
}

Цикл for r := range in почти читается как рабочая инструкция: пока входной канал отдаёт ресурсы, обработчик берёт следующий. Канал закрыли — цикл завершился. Отдельная переменная вроде polling тут не нужна: ресурс находится у Poller только во время обработки, а после отправки в out снова принадлежит следующей части конвейера.

В таком Resource хранятся только данные, нужные самой проверке. Для URL достаточно строки; сведения о занятости и времени последнего запуска в структуре ни к чему. Например, Poller получает *Resource с адресом, проверяет его и отправляет указатель дальше, не добавляя объекту служебный флаг.

Код становится короче не потому, что из него выбросили логику. Ответственность за порядок работы теперь лежит на каналах: один участок программы выдаёт ресурсы, другой обрабатывает их и передаёт результат дальше. Полную схему с запуском нескольких обработчиков и закрытием каналов лучше разбирать отдельно. Сам Poller остаётся небольшим и занимается одной операцией.

Вывод

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

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

Практический совет: разбирая конкурентный код, выпишите для каждого значения его владельца на текущем шаге. Если несколько горутин одновременно претендуют на один объект, попробуйте передавать его через канал; если так не получается, только тогда вводите блокировку. Полный разбор подхода есть в Codewalk Share Memory By Communicating.

Похожее