В первой части статьи мы остановились на моменте, когда с помощью распределения задач между потоками по алгоритму Round-robin мы добились-таки ускорения работы приложения за счет многопоточности. Но вот неприятность: такой алгоритм очень неравномерно нагружает потоки и не полностью утилизирует их возможности - пока кто-то простаивает, другой уже копит очередь. Как это можно обойти? Давайте попробуем вместо "запихивания" задачи в очередь каждого конкретного потока сформировать общую очередь для некоторого пула потоков: при получении задачи отдаем ее первому свободному потоку или ставим в общую очередь, когда таких нет; при освобождении потока сразу даем ему следующую задачу из очереди. В качестве такой очереди можно использовать и обычный массив. Но лучше не стоит из-за проблем производительности Array.shift(), описанных в прошлой статье - так что возьмем реализацию на основе кольцевого буфера, приведенную там же. Основным отличием от схемы с Round-robin, приведенной в прошлой части, является попадание следующей задачи в поток только когда он уже готов ей заниматься, причем сразу при завершении предыдущей. Модификации в нашем коде будут достаточно невелики - само описание класса WorkersPool: ... и пара мест его использования:  При количестве активных потоков не меньше количества ядер процессора реализация с общей очередью выигрывает по времени при более равномерной и существенно меньшей средней утилизации потоков. Впрочем, в этой радужной картинке есть и толика печали. Средняя нагрузка на потоках уменьшилась ровно за счет того, что мы не можем их теперь эффективно загрузить - особенно хорошо это видно при малом их количестве. Это объясняется тем, что нагрузка на основной поток заметно выросла из-за необходимости  не только реагировать на прикладную активность, но и заниматься обработкой очереди - как минимум, реагировать на каждый ответ, отмечая служебный поток как свободный. Но ведь мы запросто можем разделить каналы обмена с потоком, на канал данных и канал сигналов как отдельные экземпляры MessageChannel. И будем передавать сигнал освобождения потока только однократно при обработке сразу пачки задач, получаемых через receiveMessageOnPort. Схематично MessageChannel может быть представлен, как два порта, один из которых принадлежит основному (или любому другому) потоку, а другой - вспомогательному. В каждый из них можно как неограниченно писать, поскольку за ним стоит очередь, так и читать из него - получая события или вычитывая самостоятельно. Собственно, доступный нам на стороне потока parentPort является ровно такой же "половинкой" встроенного по умолчанию в Worker экземпляра  MessageChannel. В отличие от предыдущего варианта, очередь отправляется в дата-порт конкретного потока сразу блоками из нескольких задач, а основной поток формирует следующую порцию не на каждую отдельную завершенную задачу, а при поступлении через сигнал-порт сообщения об окончании обработки всего блока. Понятно, что если задачи не идеально одинаковы, отправляя сразу блоками, мы снова можем какие-то из потоков перегрузить. Поэтому будем стараться отдавать потоку не больше 1/N от остающейся длины очереди. Соответственно, теперь нам достаточно вместо пула потоков иметь пул дата-каналов (точнее, их портов) для отправки данных и подписку на сигнальный порт: Для передачи порта обмена в поток мы воспользовались такой структурой как transferList. В отличие от остальных элементов сообщения, упомянутые в нем объекты не копируются, а передаются потоку в готовом виде как структура в разделяемой памяти. Чтобы исключить конфликты доступа, на стороне передающего они при этом разрушаются и в дальнейшем не могут быть использованы. Остальной код никак не меняется, разве что деструктор для очистки подписок теперь звать не обязательно.  На малом количестве потоков почти вышли на показатели Round-robin, а вот на большом иногда существенно "штормит" от неравномерности распределения очереди. В общем, не можем признать данный опыт однозначно положительным. Давайте попробуем объединить оба предыдущих метода. В работе с очередью нас не устраивали задержки обработки готовности задач на стороне основного потока - так давайте ее вообще вынесем в другой, специализированный, поток - координатор, который сам будет распределять задачи между рабочими потоками. В коде мы сразу через workerData передаем сигнальный канал и тип потока. Для этого придется немного модифицировать код запуска рабочих потоков:  А потом уже через этот сигнальный канал передаем порт обмена между координатором и worker'ом: А вот код самих потоков поменяется намного сильнее. Обратите внимание на конструкцию switch-case - в многопоточных приложениях приходится применять ее достаточно часто как для разделения логики различных типов потоков, так и при обработке разных типов сообщений: Здесь мы организуем все подписки, чтобы задача, попавшая в координатор, была выдана первому же свободному (или освободившемуся) потоку, который потом сам скинет результат в parentPort основному потоку, сообщив координатору о своей доступности:  И... результаты хоть и радуют равномерностью распределения нагрузки между потоками, вообще не впечатляют с точки зрения общей производительности. То есть такой подход вполне допустим, если само формирование задачи для потока или обработка ее выполнения являются вычислительно-сложными - тогда их имеет смысл выносить в координатор. Но в большинстве типовых случаев такой подход показывает себя не лучшим образом. А все потому, что, с одной стороны, мы сначала копируем контент задачи из основного потока в координатор, потом из координатора в рабочий поток, а с другой - worker бесцельно ждет, когда мы отреагируем на его готовность. И чем больше активных потоков участвует в такой схеме, тем большую долю времени они вынужденно простаивают. А нельзя ли как-то сделать, чтобы потоки забирали и распределяли задачи между собой "сами", не ожидая бесцельно, пока им кто-то это скомандует? Оказывается, можно, но про это - в следующей части. часть 1: базовые концепты часть 2: очередь, каналы и координатор часть 3: разделяемая память, атомарные операции и блокировки часть 4: координатор против синхронного кода часть 5: автомасштабирование под нагрузку  Архитектура ИС: PostgreSQL, Node.js и highload