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

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

Веб-воркеры приходят на помощь в этих ужасных ситуациях. Веб-воркеры были представлены Javascript в 2009 году и до сих пор являются экспериментальной функцией в некоторых браузерах, например. Mozilla Firefox. Но использование Web Workers может полностью изменить производительность вашей веб-страницы, особенно когда на стороне клиента задействованы тяжелые вычисления.

Итак, что такое веб-воркеры?

Веб-воркер - это сценарий, который выполняется не в основном потоке, а в другом потоке. Сценарий Web Worker может быть запущен веб-страницей, на которой запущен javascript. Вызванный сценарий затем запускается в отдельном потоке, который порождается из основного потока и, следовательно, снимает нагрузку с основного потока. Два потока могут обмениваться сообщениями с помощью API postMessage.
В зависимости от варианта использования также доступны 2 типа веб-воркеров, а именно:

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

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

Где я их использовал?

Я столкнулся с веб-воркерами при решении заведомо сложной проблемы в приложении, в котором использовался Tensorflow.js. Мы использовали AI-библиотеку для распознавания лиц, созданную поверх Tensorflow.js, для разработки приложения для контроля. Мы столкнулись с проблемами при интеграции библиотеки, потому что модель была большой и долго загружалась в браузере. Более того, алгоритм вычисления обнаружений был длительным и ресурсоемким процессом и, как следствие, приводил к сбою браузера или к полному зависанию системы. Веб-воркеры были ответом на нашу затруднительную ситуацию. Мы использовали Web Workers для создания отдельного контекста выполнения и запускали в нем алгоритм обнаружения лиц. Это привело к тому, что основной поток не был перегружен всеми тяжелыми вычислениями и беспрепятственно обрабатывал другие события и процессы.

Как мы внедряем Web Workers?

Хорошо, достаточно объяснений о веб-воркерах, давайте приступим к практической реализации одного из них. Web Workers довольно просто реализовать. Вам понадобится как минимум 2 файла, один из которых будет основным скриптом, который будет запускаться в основном потоке. Другой будет рабочий сценарий, который будет запускаться внутри выделенного веб-исполнителя. Лучше всего хранить рабочий сценарий в отдельном файле, так как он будет запускаться в другом и изолированном контексте выполнения.

Назовем файлы mainFile.js и webWorker.js соответственно.

Shared Web Worker может быть создан почти таким же образом, с той лишь разницей, что конструктор используется во время создания экземпляра. Мы используем конструктор SharedWorker() для создания общего веб-воркера.

Если указанный файл существует, он будет загружен асинхронно, а в противном случае Worker выйдет из строя, а ваше веб-приложение будет работать даже в случае ошибки 404.

Теперь, когда у нас есть выделенный Web Worker, давайте общаться с ним, отправляя сообщение из основного потока. Веб-воркер и основной поток взаимодействуют с помощью API postMessage ().

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

В приведенном выше примере показано, как Web Worker прослушивает сообщения из основного потока через API onmessage ().

Однако есть разница в способе общения с Shared Web Worker из основного потока.

С Shared Web Worker ожидается, что мы будем общаться через объект port - открывается явный порт, через который скрипты могут связываться с экземпляром worker (это делается неявно в случае выделенных worker). Нам также необходимо явно вызвать функцию start(), чтобы установить связь.

Настройка слушателя в потоке Shared Worker также немного отличается. Рассмотрим следующее -

Как можно заключить из приведенного выше примера, сценарий Shared Web Worker должен прослушивать событие theconnect, чтобы всякий раз, когда какой-либо внешний контекст запрашивает соединение, рабочий сценарий мог ответить соответствующим образом.

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

Мы также можем завершить запущенный экземпляр выделенного веб-воркера из основного потока, вызвав функцию terminate () -

Экземпляр Web Worker немедленно уничтожается вместе со всеми его текущими операциями, если таковые имеются.

Точно так же в случае Shared Web Workers мы можем закрыть соединение, вызвав функцию port.close(), рабочий поток будет немедленно убит без возможности завершить свои операции или очистить после себя.

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

Приведенные выше примеры реализуют простой Web Worker и демонстрируют его взаимодействие с основным потоком. Реальные сценарии, требующие использования Web Workers, вряд ли будут такими легкими, поэтому мы должны знать о предостережениях, связанных с реализацией Web Workers.

В чем подвох?

Веб-воркеры не могут получить доступ к DOM -

Поскольку веб-воркеры работают в отдельном контексте выполнения, они не имеют доступа к элементам DOM. Никто не сможет управлять DOM, обращаться к глобальным переменным из основного потока или запускать его функции. Также нельзя получить доступ к методам и свойствам по умолчанию объекта window. Однако у вас есть доступ к таким функциям, как setTimeout () и setInterval (). Вы также можете использовать объект XMLHttpRequest, чтобы делать запросы Ajax к серверу. Вам также разрешено использование WebSockets и механизмов хранения данных, таких как IndexedDB. Доступ к контексту внутри рабочего потока можно получить через DedicatedWorkerGlobalScope или SharedWorkerGlobalScope в зависимости от типа Используемый рабочий. Первый работает с выделенным веб-рабочим, а второй - с общим веб-рабочим соответственно.

Политика безопасности контента -

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

Объем данных, передаваемых веб-воркерам, может повлиять на производительность. Кроме того, поддерживаемые типы ограничены -

Объем данных, которые вы передаете Web Worker, имеет прямое влияние на производительность вашей веб-страницы. Как уже говорилось, веб-воркеры имеют отдельный контекст выполнения, что означает, что у них также есть отдельная память и стек вызовов, и они не могут получить доступ к переменным из основного потока. Таким образом, вся информация, передаваемая в рабочий поток, клонируется или дублируется с использованием алгоритма структурированного клонирования. Этот алгоритм рекурсивно просматривает данные (обычно переданные как объект). Хотя это может не вызывать проблем с небольшими наборами данных, передача большого объекта с многочисленными глубоко вложенными парами ключ-значение рабочему значительно замедлит межпотоковое взаимодействие, а также потребляет все ваши вычислительные ресурсы, непреднамеренно принося больше вреда, чем пользы. Также существуют ограничения на тип данных, которые принимает Web Worker. Вещи, которые нельзя клонировать с помощью алгоритма структурированного клонирования, нельзя передать Web Worker ex. функции, узлы DOM и некоторые свойства также теряются при клонировании. Один из способов обойти клонирование объектов - использовать передаваемые объекты, такие как SharedArrayBuffer.

Обеспечьте атомарные обновления для передаваемых объектов, таких как SharedArrayBuffer -

Многопоточность не может быть достигнута без предоставления общей памяти. Если нужно копировать и вставлять всю структуру данных каждый раз, когда требуется изменение, тогда вся идея запуска кода в отдельных потоках бессмысленна. Для этого тоже есть обходной путь, который называется SharedArrayBuffer. SharedArrayBuffer - это объект, используемый для представления универсального буфера необработанных двоичных данных фиксированной длины, как объяснил MDN. Это означает, что вместо отправки данных и их клонирования из основного потока мы можем обновить одну и ту же общую память в обоих потоках. Это позволяет обоим потокам иметь ссылку на разделяемую память, что вообще устраняет необходимость клонирования данных. Вот так -

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

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

При совместном использовании памяти несколькими потоками мы можем столкнуться с ситуацией, называемой Состояние гонки. Состояние гонки может возникнуть, когда несколько потоков пытаются одновременно изменить одни и те же данные. Это приводит к устаревшим и противоречивым данным. Это вводит понятие объекта Атомика.

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

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

Выводы

Многопоточность в JavaScript постепенно набирает обороты и популярность. Перспектива разгрузки работы на несколько потоков и совместного использования памяти между ними имеет огромный потенциал. На мой взгляд, идея SharedArrayBuffers и многопоточности полезна, особенно если речь идет о сервисах NodeJS. Что касается поддержки кроссбраузерности, она все еще находится в зачаточном состоянии. Поддержка Worker Threads все еще является экспериментальной в большинстве браузеров, и Google Chrome является одним из совместимых. Многопоточность Javascript является многообещающей и может изменить возможности традиционных веб-приложений. Мы можем привнести сложную анимацию, интеграцию ИИ и более сложные вычислительные мощности в клиентские приложения с помощью веб-работников. По мере того, как их внедрение постепенно увеличивается, мы можем увидеть больше таких в сети, а пока никогда не пропадёт возможность учиться и пробовать что-то новое.

Удачного кодирования !!