Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Go

Пойдем: Параллельность Голанга, часть 2

by
Read Time:8 minsLanguages:
This post is part of a series called Let's Go: Golang Concurrency.
Let's Go: Golang Concurrency, Part 1

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Обзор

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

Что такое канал?

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

Вы создаете канал с помощью make() и указываете тип значений, которые принимает канал:

ch: = make (chan int)

Go предоставляет приятный синтаксис стрелок для отправки и получения в/из каналов:

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

<-ch

Каналы блокируются по умолчанию. Если вы отправите значение в канал, вы заблокируете его, пока кто-нибудь не получит его. Точно так же, если вы получаете от канала, вы будете блокировать, пока кто-то не отправит значение в канал.

Следующая программа демонстрирует это. Функция main() создает канал и запускает вызываемую процедуру go, которая печатает «start», считывает значение из канала и печатает. Затем main() запускает другую процедуру, которая просто печатает тире ("-") каждую секунду. Затем она спит в течение 2,5 секунд, отправляет значение в канал и спит еще 3 секунды, чтобы завершить выполнение всех процедур.

Эта программа очень хорошо демонстрирует блокирующий характер канала. Первая программа распечатывает «start» сразу, но затем блокируется при попытке получить от канала функцию main(), которая спит в течение 2,5 секунд и отправляет значение. Другая программа просто обеспечивает визуальное представление о потоке времени, регулярно печатая тире каждую секунду.

Вот вывод:

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

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

Буферизованные каналы - это каналы, которые могут содержать определенное (предопределенное) количество значений, чтобы отправители не блокировались до тех пор, пока буфер не будет заполнен, даже если никто ничего не получает.

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

ch: = make (chan int, 5)

Следующая программа иллюстрирует поведение буферизованных каналов. Программа main() определяет буферизованный канал с емкостью 3. Затем она запускает одну процедуру, которая каждую секунду читает буфер из канала и печатает, и другую программу, которая просто печатает тире каждую секунду, чтобы наглядно показать ход выполнения. Затем она отправляет пять значений в канал.

Что происходит во время выполнения? Первые три значения сразу буферизуются каналом и функциональными блоками main(). Через секунду значение получено программой, и функция main() может выдвинуть другое значение. Проходит еще одна секунда, программа получает другое значение, и функция main() может выдвинуть последнее значение. На этом этапе программа продолжает получать значения из канала каждую секунду.

Вот вывод:

Выбор

Буферизованные каналы (если буфер достаточно большой) могут решить проблему временных колебаний, когда не хватает приемников для обработки всех отправленных сообщений. Но есть и противоположная проблема заблокированных получателей, ожидающих обработки сообщений. Go решает эту проблему.

Что если вы хотите, чтобы ваша программа выполняла что-то еще, когда в канале нет сообщений для обработки? Хороший пример, если ваш получатель ожидает сообщений от нескольких каналов. Вы не хотите блокировать на канале A, если канал B имеет сообщения прямо сейчас. Следующая программа пытается вычислить сумму 3 и 5, используя полную мощность машины.

Идея состоит в том, чтобы моделировать сложную операцию (например, удаленный запрос к распределенной БД) с избыточностью. Функция sum() (обратите внимание, как она определена как вложенная функция внутри main()) принимает два параметра int и возвращает канал int. Внутренняя анонимная программа неактивна некоторое время до одной секунды, а затем записывает сумму в канал, закрывает и возвращает его.

Теперь main вызывает sum(3, 5) четыре раза и сохраняет результирующие каналы в переменных от ch1 до ch4. Четыре вызова sum() возвращаются немедленно, потому что случайный спящий процесс происходит внутри процедуры, которую вызывает каждая функция sum().

Здесь начинается классная часть. Оператор select позволяет функции main() ждать на всех каналах и отвечать на первый, который возвращается. Оператор select работает немного как оператор switch.

Иногда вы не хотите, чтобы функция main() блокировала ожидание даже до завершения первой процедуры. В этом случае вы можете добавить вариант по умолчанию, который будет выполняться, если все каналы заблокированы.

Пример веб-сканера

В моей предыдущей статье я продемонстрировал решение упражнения для веб-сканера Tour of Go. Я использовал горутины и синхронизированную карту. Я также решил упражнение, используя каналы. Полный исходный код для обоих решений доступен на GitHub.

Давайте посмотрим на соответствующие части. Во-первых, вот структура, которая будет отправляться на канал всякий раз, когда программа обрабатывает страницу. Он содержит текущую глубину и все URL-адреса, найденные на странице.

Функция fetchURL() принимает URL, глубину и канал вывода. Она использует сборщик (предоставленный в упражнении) для получения URL-адресов всех ссылок на странице. Он отправляет список URL-адресов в виде одного сообщения на канал кандидата в виде структуры links с уменьшенной глубиной. Глубина показывает, насколько дальше мы должны ползти. Когда глубина достигает 0, дальнейшая обработка не производится.

Функция ChannelCrawl() координирует все это. Она отслеживает все URL, которые уже были извлечены на карте. Нет необходимости синхронизировать доступ, потому что никакая другая функция или процедура не касаются. Она также определяет канал-кандидат, в который все goroutines будут записывать свои результаты.

Затем он запускает parseUrl в качестве goroutines для каждого нового URL. Логика отслеживает, сколько гоу-рутин было запущено с помощью счетчика. Всякий раз, когда значение считывается из канала, счетчик уменьшается (так как отправляющая процедура завершается после отправки), и всякий раз, когда запускается новая процедура, счетчик увеличивается. Если глубина становится равной нулю, то никакие новые подпрограммы не будут запущены, и основная функция будет продолжать чтение из канала, пока все подпрограммы не будут выполнены.

Заключение

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.