Базовые знания по следующим темам обязательны для понимания фрагментов кода в этой статье.
- Конкуренция.
- Горутины (эквивалент потоков в golang).
- Каналы.
Пример 1
Для примера рассмотрим ресторан. В ресторане есть несколько официантов и поваров.
Обычно взаимодействие между клиентами, официантами и поварами в ресторане выглядит следующим образом:
Официант принимает заказ от клиента.
Официант отдает заказ какому-то повару.
Шеф-повар готовит заказ.
Шеф-повар отдает приготовленное блюдо какому-нибудь официанту (не обязательно тому же официанту, который принимал заказ).
Официант подает блюдо клиенту.
Как представить этот процесс в коде?
Допустим, у нас есть N клиентов, тогда мы будем обслуживать клиентов одного за другим линейным способом. Официант X примет заказ клиента 1, отдаст его какому-нибудь повару Y. Шеф-повар Y приготовит блюдо и отдаст его какому-то официанту Z. Официант Z принесет это блюдо покупателю 1. Затем тот же процесс произойдет для клиентов 2, 3 … N. См. вывод вышеуказанной программы ниже.
В чем недостаток управления рестораном с использованием вышеуказанной стратегии?
Если много посетителей приходят в ресторан примерно в одно и то же время, многим из них придется ждать, чтобы даже отдать свой заказ официанту. В настоящее время ресторан не может использовать весь потенциал своего персонала (официантов и поваров). Другими словами, используя эту стратегию, он не сможет хорошо масштабироваться или обслуживать большое количество клиентов за меньшее время.
Решение
Конкуренция! Когда один официант или повар занят обслуживанием какого-то клиента, другие официанты или повара не будут сидеть без дела. Они будут обслуживать других клиентов. Таким образом, ресторан может обслуживать несколько клиентов одновременно. Мы будем использовать горутины для реализации решения. Принятие заказа (горутина 1), приготовление пищи (горутина 2) и возврат готового блюда (горутина 3)… теперь все это будет происходить одновременно.
Звучит неплохо, правда? Но у у нас теперь новая проблема!
См. вывод вышеуказанной программы ниже.
Из приведенного выше вывода видно, что иногда конкретный заказ готовится еще до того, как его примет официант! Или заказ будет доставлен еще до того, как его приготовят или примут! Хотя официанты и повара сейчас работают параллельно, но должна быть зафиксирована последовательность, в которой заказ должен быть принят, приготовлен и принесен (взять -> приготовить -> принести).
Как это исправить?
Нам нужно синхронизировать связь между разными горутинами. Повара не должны начинать готовить заказ до получения заказа от какого-нибудь официанта. И официант не должен доставлять заказ до получения его от шеф-повара. Мы будем использовать каналы здесь. Каналы по своей природе подобны очередям сообщений. Создадим два канала. Один для взаимодействия между поварами и официантами, которые принимают заказы от клиентов и доставляют их шеф-повару. И еще один для взаимодействия поваров и официантов, которые доставляют готовое блюдо клиенту.
Всякий раз, когда официант принимает какой-либо заказ, он отправляет заказ в канал заказа (очередь). Всякий раз, когда блюдо готово, шеф-повар отправляет заказ в канал доставки (в очередь). Официанты заберут приготовленный заказ с начала этой очереди и доставят его нужному клиенту.
Допустим, всякий раз, когда вызывается какой-то конкретный API вашего веб-сервиса, он выполняет несколько одновременных вызовов некоторой внешней службы. Для работы этого запроса выделяется несколько горутин. Внешний сервис может быть любым (может быть сервисом AWS).
Примечание. Здесь мы используем конкуренцию, потому что хотим, чтобы задержка нашего API была как можно меньше. Без конкуренции нам придется итеративно совершать вызовы внешней службы.
Как предотвратить троттлинг?
Допустим, наша служба в настоящее время делает N обращений к внешней службе для каждого запроса к нашему API. Вместо выделения N горутин мы будем использовать пул горутин или рабочий пул из M горутин (M < N, M = N/X). Теперь в каждый конкретный момент времени мы отправляем во внешний сервис максимум M запросов вместо N.
Пул будет прослушивать канал заданий. Рабочие процессы будут брать задания (выполнять вызовы во внешнюю службу) для выполнения из передней части канала (очереди). Как только worker закончит работу, он отправит результат в канал результатов (очередь). Как только все задания будут выполнены, мы вычислим и отправим окончательный результат вызывающей стороне API.