Параллельное программирование в Go

Параллельное программирование в Go

The Go Playground – интерактивный веб-сервис, который позволяет запускать в песочнице небольшие программы в духе « Hello world !». Попробуйте!

Изучите основы Go

A Tour of Go – еще один интерактивный учебник с кучей примеров. Он берет начало на официальном сайте и обучает вас основам программирования Go в браузере.

Установите инструменты Go

В Getting Started объясняется, как установить инструменты Go. Доступны бинарные пакеты для FreeBSD, Linux, Mac OS X и Windows, а также инструкции по развертыванию и настройке.

Начните проект Go

How to Write Go Code посвящен разработке простых пакетов Go. Он рассказывает про организацию и тестирование кода, а также про использование команд fetch , build и install .

Горутины

Вы можете создать новый поток (горутину) с помощью оператора go. Все горутины в одной программе используют одно и то же адресное пространство.

Программа выводит сообщение «Hello from main goroutine». Она также может напечатать «Hello from another goroutine», в зависимости от того, какая из двух горутин завершится первой.

Следующая программа скорее всего выведет «Hello from main goroutine» и «Hello from another goroutine». Они могут появиться в любом порядке. Еще одна особенность заключается в том, что вторая горутина работает очень медленно и не печатает сообщение до завершения программы.

Вот более реалистичный пример, где определяется функция, которая использует concurrency для отсрочки события:

Вот как вы можете использовать функцию Publish :

Скорее всего программа напечатает три строки в заданном порядке с пятисекундными перерывами между ними.

Невозможно реализовать ожидание потоков в процессе «сна», но есть метод синхронизации – использование каналов .

Реализация

Внутри горутины действуют как корутины, которые мультиплексируются между несколькими потоками операционной системы. Если одна горутина блокирует поток ОС, например, ожидая ввода, другие горутины в этом потоке будут мигрировать, чтобы продолжать работать.

Каналы обеспечивают синхронизированную связь

Новое значение канала можно задать с помощью встроенной функции make .

Чтобы отправить значение в канал, используйте бинарный оператор « <- », а для получения – унарный оператор.

Оператор задает направление канала на отправку или получение. По умолчанию канал является двунаправленным.

Буферизованные и небуферизованные каналы

  • Если пропускная способность канала равна нулю или отсутствует, канал не буферизуется и отправитель блокируется до тех пор, пока получатель не получит значение.
  • Если канал имеет буфер, отправитель блокируется только до тех пор, пока значение не будет скопировано в буфер. Если буфер заполнен, ждем пока какой-либо получатель не получит значение.
  • Приемники всегда блокируются, пока не появятся данные для приема.
  • Отправка или получение с nil-канала блокируется навсегда.

Закрытие канала

Функция закрытия помечает, что никакие значения больше не будут отправляться по каналу. Обратите внимание, что закрывать канал необходимо только в том случае, если приемник этого ожидает.

  • После вызова close и после получения любых ранее отправленных значений, операции приема вернут нулевое значение без блокировки.
  • Операция приема множества значений дополнительно возвращает состояние канала.
  • Отправка в закрытый канал или его закрытие, а также закрытие nil-канала, вызовут run-time panic .

Пример

В следующем примере функция Publish вернет канал, который используется для броадкастинга сообщения после публикации текста:

Обратите внимание: мы используем канал пустых структур для указания, что канал будет использоваться только для сигнализации, а не для передачи данных. Выглядит это так:

Select ожидает группы каналов

Оператор select одновременно ожидает нескольких операций отправки или получения.

  • Оператор блокируется до тех пор, пока одна из операций не будет разблокирована.
  • Если выполняется несколько операций, то одна из них будет выбрана случайным образом.

Операции отправки и приема в nil-канале блокируются навсегда. Это можно использовать для отключения канала в инструкции select :

Вариант по умолчанию

Вариант по умолчанию будет выполнен, если все остальные заблокированы.

Примеры

Бесконечная случайная двоичная последовательность

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

Операция блокировки по таймауту

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

Оператор select блокируется до тех пор, пока по крайней мере один cas e не сможет выполниться. С нулевыми кейсами этого никогда не произойдет:

Гонки данных

Такая ситуация возникает часто и может усложнить отладку.

Показанная ниже функция приводит к гонке данных, и ее поведение не определено – она может, например, напечатать число 1. Попробуем выяснить, как это происходит:

Две горутины g1 и g2, участвуют в гонке, и нет никакого способа узнать, в каком порядке будут выполняться операции. Ниже приведен один из нескольких возможных вариантов:

Параллельное программирование в Go

Как избежать гонки данных?

Предпочтительный способ обработки одновременного доступа к данным в Go – использовать канал для передачи данных от одной горутины к следующей.

В этом коде канала происходят два события:

  • передаются данные от одной горутины к другой – точка синхронизации;
  • отправляющая горутина будет ждать, пока другая получит данные и наоборот.

Как обнаружить гонку данных?

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

Пример

Программа с гонкой данных:

Запуск этой программы с параметром -race покажет нам, что существует гонка между записью в строке 7 и чтением в строке 9:

Подробности

Он работает на darwin/amd64, freebsd/amd64, linux/amd64 и Windows/amd64.

Накладные расходы варьируются, но обычно происходит увеличение использования памяти в 5-10 раз и увеличение времени выполнения в 2-20 раз.

Как отлаживать deadlock-и

Дэдлоки возникают, когда горутины ждут друг друга и ни одна из них не может завершиться.

Взглянем на пример:

Программа застрянет на операции отправки, ожидая вечно, пока кто-то прочитает значение. Go способен обнаруживать подобные ситуации во время выполнения. Вот результат нашей программы:

Советы по отладке

Горутина может застрять:

  • когда она ждет канал;
  • либо когда она ждет одну из блокировок в пакете sync.
  • ни одна горутина не имеет доступа к каналу или блокировке;
  • горутины ждут друг друга.

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

Ожидание горутин

Группа sync.WaitGroup ожидает завершения работы группы горутин:

  • сначала основная горутина вызывает Add, чтобы установить количество ожидающих горутин;
  • затем запускаются две новые горутины и вызывают Done при завершении.

В то же время Wait используется для блокировки до тех пор, пока эти две горутины не завершатся.

Замечание: группа ожидания не должна копироваться после первого использования.

Трансляция сигнала по каналу

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

Обратите внимание, что мы используем канал пустых структур: struct<> . Это явно указывает на то, что канал предназначен только для сигнализации, а не для передачи данных.

Вот как можно это использовать:

Как убить горутину

Чтобы горутина остановилась, ей необходимо прослушивать сигнал остановки на выделенном выходном канале и проверять его.

Вот более полный пример, где используется один канал как для передачи данных, так и для сигнализации:

Timer и Ticker

Таймеры и тикеры позволяют выполнять код по расписанию один или несколько раз.

Timeout (Timer)

time.After ожидает в течение заданного промежутка, а затем отправляет текущее время по возвращаемому каналу:

time.Timer не будет обработан сборщиком мусора до тех пор, пока таймер не сработает. Используйте time.NewTimer вместо вызова метода Stop , когда таймер больше не нужен:

Repeat (Ticker)

time.Tick возвращает канал, который обеспечивает тиканье часов с четными интервалами:

time.Ticker не будет обработан сборщиком мусора до тех пор, пока таймер не сработает. Используйте time.NewTicker вместо вызова метода Stop , когда тикер больше не нужен:

Блокировка взаимного исключения (мьютекс)

Иногда удобнее синхронизировать доступ к данным с помощью явной блокировки, а не с помощью каналов. Стандартная библиотека Go предлагает для этой цели блокировку взаимного исключения sync.Mutex .

Используйте с осторожностью

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

В этом примере мы создаем безопасную и простую в использовании конкурентную структуру данных AtomicInt , в которой хранится integer . Любое количество горутин может безопасно получить доступ к этому числу с помощью методов Add и Value .

Заключение

Мы рассмотрели распространенные проблемы, относящиеся к конкурентности в Go . Это не весь материал по теме – остальное вам придется самостоятельно изучать на официальном сайте. Не ленитесь, развивайтесь и удачи в обучении!