Разбираем конкурентность в Go: книги, блоги, выступления
Особенность Go — удобный механизм конкурентности. Создавать конкурентные задачи в парадигме языка можно буквально «бесплатно» и предельно просто: достаточно написать ключевое слово go перед вызовом функции — и она начнет выполняться в отдельном потоке.
Но, чтобы такой код приносил пользу, необходимо правильно управлять синхронизацией: собирать результаты, корректно обрабатывать ошибки и учитывать возможные сценарии выполнения. На практике этому аспекту часто уделяют недостаточно внимания.
Я Владислав Белогрудов, эксперт по разработке ПО в YADRO. В свое время изучал различные источники и лучшие практики в поиске эффективных способов организации параллельных процессов в Go. Делюсь ими с вами.
Выступление Роба Пайка про конкурентность в Go
Горутина — это легкий поток выполнения, который создается с помощью слова go перед вызовом функции. Она запускается асинхронно и не требует отдельного потока ОС — runtime Go самостоятельно решит, как распределять горутины по имеющимся в наличии потокам. Горутины работают в одном адресном пространстве и управляются планировщиком Go, что делает их гораздо легче, чем системные потоки.
Во многих проектах можно встретить код, в котором сложно разобраться: какие горутины и где запущены, кого и когда нужно ожидать, какие каналы используются для передачи данных и как они закрываются. Это создает хаос и усложняет поддержку системы.
Первое, с чего стоит начать изучение конкурентности в Go, — выступление Роба Пайка «Конкурентность — это не параллелизм». Эта вводная лекция о том, как устроена конкурентность в языке, длится около получаса. Ее лучше посмотреть в первую очередь, поскольку материал поможет лучше понять концепции, заложенные в Go.
Блог Дейва Чени
Полезные материалы про конкурентность в Go можно найти в блоге Дейва Чени. Дейв, в целом, рассматривает разные решения в Go: как они появились и как ими пользоваться. И тему конкурентности он не обошел стороной. Например, в статье Curious Channels Дэйв Чейни рассказывает о двух важных свойствах каналов в Go:
Закрытый канал не блокируется: после закрытия в него нельзя отправлять данные, но можно читать. Если данных нет, возвращается нулевое значение. Это удобно при использовании range, который автоматически завершает цикл.
Закрытие канала уведомляет все горутины: вместо отправки сигнала каждой горутине достаточно закрыть канал, и все ожидающие горутины получат сигнал завершения.
В отличие от языков вроде C, где для управления потоками используются блокировки и разделяемая память, в Go конкурентность реализована через более простые и безопасные инструменты. Однако, несмотря на удобство, работа с каналами требует понимания нюансов, о которых Дэйв Чейни подробно рассказывает в своих статьях.
Подробнее про сравнение подходов к реализации пула потоков на языках программирования C и Go читайте в статье.
Продвинутые паттерны от Google
Доклад Advanced Go Concurrency Patterns представил Самир Аджмани из Google на конференции Google I/O 2013. Автор рассказал, как эффективно управлять конкурентными процессами с помощью горутин и каналов. Главные темы его выступления:
→ Горутины и каналы в Go
Основная идея — не использовать общую память и низкоуровневые примитивы, а передавать данные через каналы. Горутины работают независимо и требуют очень мало ресурсов.
→ Шаблоны проектирования для конкурентных программ
Обсуждаются несколько шаблонов, включая:
Worker Pool — распределение задач между горутинами.
Fan-in и Fan-out — управление данными из нескольких источников или их распределение.
Pipeline — последовательная обработка данных через цепочку горутин.
→ Обработка периодических событий и отмена операций
Go предоставляет механизмы для выполнения задач через равные промежутки времени и для отмены операций, используя таймеры, тикеры и контексты.
→ Использование select для управления конкурентностью
Оператор select позволяет управлять несколькими каналами одновременно и обрабатывать события, не блокируя выполнение.
→ Предотвращение deadlock и обработка ошибок
Deadlock можно избежать, контролируя закрытие каналов и грамотно используя механизмы синхронизации. Важно предусматривать обработку ошибок в конкурентных программах.
Актуальная статья из 2014 года
Статья Go Concurrency Patterns: Pipelines and cancellation разбирает паттерны работы с потоками в Go: объединение и разбиение потоков, worker pool, fan-in, fan-out и другие. Хотя ее написали в 2014 году, в ней много полезных идей с примерами, которые могут пригодиться изучающим конкурентность в Go.
Автор демонстрирует, как с помощью примитивов конкурентности Go можно создавать эффективные конвейеры для обработки потоковых данных.
В статье приводится простой пример конвейера, состоящего из трех этапов:
Генерация чисел (gen): функция принимает список чисел и преобразует его в канал, по которому эти числа отправляются.
Возведение в квадрат (sq): функция принимает числа из входящего канала, возводит их в квадрат и отправляет результаты в исходящий канал.
Потребление результатов: функция main настраивает конвейер и выводит результаты, полученные из предыдущего этапа.
Книжная классика в Go
Concurrency in Go: Tools and Techniques for Developers, Кэтрин Кокс-Будей
Книга предлагает глубокое погружение в тему параллельного программирования на языке Go. Автор понятно объясняет принципы работы с конкурентным кодом, а также дает рекомендации по его организации, чтобы избежать распространенных ошибок. В отличие от многих видео, где объяснения могут быть непонятными, в книге информация представлена в легком для усвоения и применения на практике формате.

Основные темы:
Основы горутин и каналов.
Паттерны проектирования для эффективной конкурентности.
Управление состояниями и обработка ошибок.
Практические советы и примеры реальных приложений.
Functional Programming in Go, Дилан Миус
Функциональное программирование особенно полезно для конкурентного кода. Его главный плюс — безопасность: чистые функции не изменяют внешние данные, а значит, их можно запускать параллельно без риска конфликтов. Они просто возвращают результат, а дальше уже можно решать, как его обработать.
Автор рассказывает, как применять функциональные подходы в языке Go, чтобы улучшить тестируемость, читаемость и безопасность кода.

В ней можно найти:
Основы функционального программирования и их применение в Go.
Использование функций высшего порядка и замыканий для гибкости и модульности.
Реализацию неизменяемых структур данных и их преимущества.
Функциональные паттерны для обработки ошибок и конкурентности.
Вместо заключения
Go позволяет легко запускать несколько потоков выполнения программы, а чистые функции при этом обеспечивают безопасность. Но перед тем как писать конкурентный код, стоит задуматься, действительно ли он нужен. Из-за легкости запуска горутин разработчики часто сами запутывают свои программы. И мы получаем обратный эффект — снижение производительности, потому что ресурсы тратятся на синхронизацию и сбор результатов.
Перед написанием конкурентного кода важно задать себе два вопроса:
Нужен ли он вообще? Возможно, это лишняя сложность.
Нужна ли многопоточность или можно обойтись стандартными библиотеками?
Например, есть известная библиотека от разработчика Сэмблера, которая умеет параллельно обрабатывать данные — фильтровать длинные списки, выполнять MapReduce и другие задачи. Благодаря ей не приходится изобретать «велосипеды» и писать лишний код.
А какие полезные материалы про конкурентность в Go можете назвать вы? Делитесь в комментариях.