Различные стратегии снижения накладных расходов ввода-вывода в среде с большим объемом операций ввода-вывода

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

Что такое асинхронное программирование на основе потоков?

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

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

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

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

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

Обработка завершения задачи

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

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

Шаблон выполнения выглядит примерно так → Поток 1 вызывает рабочий пул, чтобы дать ему задачу. Поток 2 в рабочем пуле выполняет задачу и вызывает обратный вызов. Поток 1 прерывается и переключается на выполнение обратного вызова.

Выполнение обратного вызова определяется языком/платформой. Различные языки могут реализовать этот шаблон выполнения по-разному. Например, в таких языках, как java/scala ExecutorService — это инфраструктура, которая отвечает за выполнение обратного вызова после будущего завершения, обратный вызов может быть выполнен либо в потоке вызывающей стороны, либо из любой поток из самого пула.

Предостережения по использованию асинхронного программирования на основе потоков

  • Асинхронный код может быть немного сложным для понимания и отладки, если у вас есть несколько параллельных и последовательных передач задач, создающих ад обратных вызовов, который является одной из самых больших проблем во всех языках асинхронного программирования.
  • Переменные ThreadLocal больше не работают. Поскольку вызывающий поток передает работу другому потоку и переходит к другим задачам, любой контекст, хранящийся как ThreadLocal, например: контекст запроса для выполняемого HTTP-запроса, теряется. Единственный способ распространить его — явно передать его в качестве параметра при передаче задачи.

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

Можно использовать настоящую парадигму неблокирующего ввода-вывода, также известную как асинхронное программирование на основе событий, например, в node js, где потоки никогда не блокируются при вводе-выводе, и нам не нужно создавать и поддерживать пулы рабочих потоков. В следующем блоге мы рассмотрим эту парадигму.

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

Отправьте мне сообщение в Linkedin или напишите мне на [email protected]

Создавайте компонуемые веб-приложения

Не создавайте веб-монолиты. Используйте Bit для создания и компоновки несвязанных программных компонентов — в ваших любимых средах, таких как React или Node. Создавайте масштабируемые и модульные приложения с мощными и приятными возможностями разработки.

Перенесите свою команду в Bit Cloud, чтобы вместе размещать компоненты и совместно работать над ними, а также ускорять, масштабировать и стандартизировать разработку в команде. Попробуйте компонуемые внешние интерфейсы с помощью Design System или Микроинтерфейсы или изучите компонуемые внутренние интерфейсы с серверными компонентами. .

Попробуйте →

Узнать больше