Горутины
Вы можете создать новый поток (горутину) с помощью оператора 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
блокируется до тех пор, пока по крайней мере один case
не сможет выполниться. С нулевыми кейсами этого никогда не произойдет:
Гонки данных
Гонка данных происходит, когда две горутины одновременно обращаются к одной и той же переменной и хотя бы одно из обращение является записью.
Такая ситуация возникает часто и может усложнить отладку.
Показанная ниже функция приводит к гонке данных, и ее поведение не определено – она может, например, напечатать число 1. Попробуем выяснить, как это происходит:
Как избежать гонки данных?
Предпочтительный способ обработки одновременного доступа к данным в Go – использовать канал для передачи данных от одной горутины к следующей.
В этом коде канала происходят два события:
передаются данные от одной горутины к другой – точка синхронизации;
отправляющая горутина будет ждать, пока другая получит данные и наоборот.
Как обнаружить гонку данных?
Гонки данных могут легко появляться, но обнаружить их трудно. К счастью среда выполнения Go может помочь и в этом. Используйте ключ -race для включения встроенного детектора гонки данных.
Пример
Программа с гонкой данных:
Запуск этой программы с параметром -race покажет нам, что существует гонка между записью в строке 7 и чтением в строке 9:
Подробности
Детектор гонки не выполняет никакого статического анализа. Он проверяет доступ к памяти во время выполнения только для фактически работающего кода.
Он работает на darwin/amd64, freebsd/amd64, linux/amd64 и Windows/amd64.
Накладные расходы варьируются, но обычно происходит увеличение использования памяти в 5-10 раз и увеличение времени выполнения в 2-20 раз.
Как отлаживать deadlock-и
Дэдлоки возникают, когда горутины ждут друг друга и ни одна из них не может завершиться.
Взглянем на пример:
Программа застрянет на операции отправки, ожидая вечно, пока кто-то прочитает значение. Go способен обнаруживать подобные ситуации во время выполнения. Вот результат нашей программы:
Советы по отладке
Горутина может застрять:
когда она ждет канал;
Общие причины:
ни одна горутина не имеет доступа к каналу или блокировке;
горутины ждут друг друга.
Сейчас Go обнаруживает только зависание всей программы в целом, а не когда застревает некое подмножество горутин.
С помощью каналов легко понять, что вызвало дедлок. С другой стороны, интенсивно использующие мьютексы программы могут быть заведомо трудными для отладки.
Ожидание горутин
Группа sync.WaitGroup
ожидает завершения работы группы горутин:
Замечание: группа ожидания не должна копироваться после первого использования.
Трансляция сигнала по каналу
В этом примере функция Publish
возвращает канал, который используется для передачи сигнала при публикации сообщения.
Обратите внимание, что мы используем канал пустых структур: struct{}
. Это явно указывает на то, что канал предназначен только для сигнализации, а не для передачи данных.
Вот как можно это использовать:
Как убить горутину
Чтобы горутина остановилась, ей необходимо прослушивать сигнал остановки на выделенном выходном канале и проверять его.
Вот более полный пример, где используется один канал как для передачи данных, так и для сигнализации:
Timer и Ticker
Таймеры и тикеры позволяют выполнять код по расписанию один или несколько раз.
Timeout (Timer)
Repeat (Ticker)
time.Tick
возвращает канал, который обеспечивает тиканье часов с четными интервалами:
Блокировка взаимного исключения (мьютекс)
Используйте с осторожностью
Чтобы этот тип блокировки был безопасным, крайне важно, чтобы все обращения к общим данным выполнялись только тогда, когда горутина находится в блокировке. Одной ошибки в одной горутине достаточно, чтобы ввести гонку данных и сломать программу.
Из-за этого вам следует подумать о разработке кастомной структуры данных с чистым API и убедиться, что вся синхронизация выполняется внутри.
В этом примере мы создаем безопасную и простую в использовании конкурентную структуру данных AtomicInt
, в которой хранится integer
. Любое количество горутин может безопасно получить доступ к этому числу с помощью методов Add
и Value
.
Last updated
Was this helpful?