Разбираем конкурентность в 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 можно создавать эффективные конвейеры для обработки потоковых данных.

В статье приводится простой пример конвейера, состоящего из трех этапов:

  1. Генерация чисел (gen): функция принимает список чисел и преобразует его в канал, по которому эти числа отправляются.

  2. Возведение в квадрат (sq): функция принимает числа из входящего канала, возводит их в квадрат и отправляет результаты в исходящий канал.

  3. Потребление результатов: функция main настраивает конвейер и выводит результаты, полученные из предыдущего этапа.

Книжная классика в Go

Concurrency in Go: Tools and Techniques for Developers, Кэтрин Кокс-Будей 

Книга предлагает глубокое погружение в тему параллельного программирования на языке Go. Автор понятно объясняет принципы работы с конкурентным кодом, а также дает рекомендации по его организации, чтобы избежать распространенных ошибок. В отличие от многих видео, где объяснения могут быть непонятными, в книге информация представлена в легком для усвоения и применения на практике формате. 

Разворот книги «Concurrency in Go: Tools and Techniques for Developers»
Разворот книги «Concurrency in Go: Tools and Techniques for Developers»

Основные темы:

  • Основы горутин и каналов.

  • Паттерны проектирования для эффективной конкурентности.

  • Управление состояниями и обработка ошибок.

  • Практические советы и примеры реальных приложений.

Functional Programming in Go, Дилан Миус

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

Автор рассказывает, как применять функциональные подходы в языке Go, чтобы улучшить тестируемость, читаемость и безопасность кода.

Разворот книги «Functional Programming in Go»
Разворот книги «Functional Programming in Go»

В ней можно найти:

  • Основы функционального программирования и их применение в Go.

  • Использование функций высшего порядка и замыканий для гибкости и модульности.

  • Реализацию неизменяемых структур данных и их преимущества.

  • Функциональные паттерны для обработки ошибок и конкурентности.

Вместо заключения

Go позволяет легко запускать несколько потоков выполнения программы, а чистые функции при этом обеспечивают безопасность. Но перед тем как писать конкурентный код, стоит задуматься, действительно ли он нужен. Из-за легкости запуска горутин разработчики часто сами запутывают свои программы. И мы получаем обратный эффект — снижение производительности, потому что ресурсы тратятся на синхронизацию и сбор результатов.

Перед написанием конкурентного кода важно задать себе два вопроса:

  1. Нужен ли он вообще? Возможно, это лишняя сложность.

  2. Нужна ли многопоточность или можно обойтись стандартными библиотеками?

Например, есть известная библиотека от разработчика Сэмблера, которая умеет параллельно обрабатывать данные — фильтровать длинные списки, выполнять MapReduce и другие задачи. Благодаря ей не приходится изобретать «велосипеды» и писать лишний код.

А какие полезные материалы про конкурентность в Go можете назвать вы? Делитесь в комментариях.

© Habrahabr.ru