Истории успеха наших клиентов — лучшие проекты
Вход/ Регистрация

Многопоточность в Golang

2117
14 минут чтения
Средний рейтинг статьи: 5

Однопоточные приложения в Golang выглядят как обычный последовательно выполняющийся код.

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

Никаких общих данных, проблем одновременного доступа (чтения и записи) и синхронизации.

Многопоточные приложения Go распараллеливают логику на несколько частей, тем самым ускоряя выполнение программы. Рабочие задачи в этом случае выполняются одновременно.

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

Простое приложение

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

В коде ниже у нас есть 2 пещеры, каждая из которых содержит уникальный набор ресурсов. У каждой пещеры есть состояние выработки, обозначающее количество совершенных копок внутри шахты:

    

В примере выше шахты копаются по очереди до тех пор, пока полностью не исчерпают ресурсы. Поэтому консольный вывод будет строго такой:

    

Обратите внимание, что сначала выкапывается шахта «Зарубки», а уже потом «Каменки». Такая последовательная (однопоточная) копка кажется довольно медленной и неэффективной.

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

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

Горутины (Goroutines)

Распараллелить выполнение нескольких задач можно с помощью так называемых «горутин» (Goroutines).

По сути «горутина» — это функция, которая не прекращает выполнение программы (кода, который указан после нее) в тот момент, когда начинает выполняться сама.

Вызвать такую параллельно выполняемую функцию довольно просто — нужно лишь добавить ключевое слово go перед самим вызовом.

Вот вам небольшой пример, состоящий из псевдокода:

    

Теперь мы можем немного модифицировать наше приложение с шахтами:

    

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

    

Здесь видно, что копка в обеих шахтах выполняется одновременно, при этом информация о результатах добычи ресурсов «разбавляется» сообщениями от «Центра обеспечения», которые периодически продуцирует основной цикл программы.

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

Каналы (Channels)

Каналы (Channels) — это своего рода «кабели», позволяющие горутинам обмениваться информацией между собой.

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

При этом для записи и извлечения данных используются символы стрелки и тире:

    

Общий консольный вывод в этом примере будет следующий:

    

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

    

Общий консольный вывод будет такой:

    

Такое использование горутин является своего рода блокирующим приемом синхронизации.

Направления каналов

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

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

Одной будет разрешено только записывать данные в канал, а другой — только читать из него:

    

Общий вывод в терминал консоли будет следующим:

    

Неблокирующее чтение из канала

За счет использования конструкции select есть возможность избежать блокировки выполнения кода при чтении из канала:

    

Модернизированное приложение

Зная, как использовать горутины и каналы, мы можем модифицировать код предыдущий программы с шахтами. Давайте реализуем следующий сценарий копки шахт.

Пускай у нас будет «Центр обеспечения», который сперва выполнит запуск всех имеющихся шахт, после чего будет ожидать их полной выработки. По завершению копки все шахты сообщат «Центру обеспечения» о том, что они завершили свою работу. Как следствие, «Центр обеспечения» завершит программу.

В коде ниже мы создадим отдельные структуры под шахты и «Центр обеспечения»:

    

Вывод этой программы будет примерно следующий:

    

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

Заключение

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

Использование «Горутин» и «Каналов» можно использовать в сочетании с различными языковыми конструкциями Go. В нашем случае мы реализовали блокировку выполнения программы внутри цикла for, тем самым обеспечив ожидание выполнения кода внутри горутин.

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

Тем не менее, главное придерживаться некоторых базовых принципов, помогающих не усложнить логику программы:

  • Отдавайте предпочтение каналам, а не обычным переменным (или указателям на них) для связи и синхронизации между горутинами.
  • Старайтесь подбирать наиболее подходящие языковые конструкции, «обрамляющие» примитивы многопоточности.
  • Не плодите лишних блокировок программы и обеспечивайте нормальное планирование при использовании процедур в Go.
  • Не создавайте многопоточные приложения «вслепую», а используйте соответствующие инструменты профилирования (например, в Go доступен специальный пакет «net/http/pprof» для оптимизации HTTP-приложений) для выявления узких мест и оптимизации производительности.
2117
14 минут чтения
Средний рейтинг статьи: 5

Читайте также

Хотите внести свой вклад?
Участвуйте в нашей контент-программе за
вознаграждение или запросите нужную вам инструкцию
img-server