⚛️🔀 Эффективная многопоточность в Node.js: как использовать Atomics

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

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

Многопоточность в Node.js может показаться сложной темой, особенно когда речь идет о безопасной работе с разделяемой памятью. В JavaScript нет встроенной поддержки многопоточности, и разработчики Node.js обычно привыкают работать с одним потоком. Однако с появлением модуля worker_threads, который позволяет создавать несколько потоков, возникла необходимость решать проблемы взаимодействия между потоками, в частности, при совместном использовании данных. Павел Романов рассказал, как это сделать с помощью Atomics.

Разделение памяти между несколькими потоками

Разделяемая память – механизм, с помощью которого несколько потоков могут обращаться к одним и тем же данным. В Node.js для этого используется объект SharedArrayBuffer. Этот объект позволяет потокам совместно использовать определенные данные, поскольку у него одна область памяти, доступная из всех потоков, что позволяет избежать дублирования данных и уменьшает затраты на память.

Когда объект типа ArrayBuffer передается от основного потока к независимому (Worker), он копируется, и каждый поток получает свою копию данных, то есть каждый поток работает с отдельной памятью. Но SharedArrayBuffer, в отличие от ArrayBuffer, позволяет нескольким потокам обращаться к одной и той же области памяти, создавая общие данные для всех потоков.

Состояние гонки

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

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

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

👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Использование Atomics для предотвращения гонки

Единственный 100% надежный способ избежать состояния гонки – использование встроенного JavaScript-объекта Atomics. Основная цель Atomics – обеспечить выполнение операций над разделяемыми ресурсами как единых, неделимых действий (атомарных операций). Это означает, что операция выполняется целиком и не может быть прервана другим потоком. Давайте перепишем код с использованием Atomics:

Здесь вместо обычной операции записи typedArray[0] = threadId используется Atomics.store, что делает операцию записи атомарной. Теперь можно быть уверенным, что каждое присваивание выполняется как единая операция, и другой поток не сможет прервать этот процесс:

Основные атомарные операции

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

  • Atomics.add() прибавляет заданное значение к текущему значению в указанной ячейке массива и возвращает предыдущее значение.
  • Atomics.and() выполняет побитовое И (AND) между текущим значением и заданным, возвращая старое значение.
  • Atomics.compareExchange() записывает значение в указанной ячейке, если текущее значение совпадает с указанным для сравнения. Возвращает предыдущее значение.
  • Atomics.exchange() записывает новое значение в указанную ячейку массива и возвращает предыдущее значение.
  • Atomics.isLockFree() проверяет, можно ли выполнить атомарную операцию без блокировок для массива данного типа. Возвращает true, если операция будет атомарной на уровне аппаратуры.
  • Atomics.load() возвращает текущее значение в указанной ячейке массива.
  • Atomics.notify() уведомляет потоки, ожидающие изменения в указанной ячейке массива. Возвращает количество уведомленных потоков.
  • Atomics.or() выполняет побитовое ИЛИ (OR) между текущим значением и заданным, возвращая старое значение.
  • Atomics.store() записывает значение в указанную ячейку и возвращает это значение.
  • Atomics.sub() вычитает заданное значение из текущего значения в указанной ячейке и возвращает предыдущее значение.
  • Atomics.wait() проверяет, что значение в указанной ячейке совпадает с заданным, и приостанавливает выполнение потока, пока значение не изменится или не истечет время ожидания. Возвращает "ok", "not-equal" или "timed-out". (Не поддерживается в главном потоке браузера.)
  • Atomics.waitAsync() ожидает асинхронно, возвращая промис, что позволяет не блокировать поток, как в случае с Atomics.wait.
  • Atomics.xor() выполняет побитовое XOR (исключающее ИЛИ) между текущим значением и заданным, возвращая старое значение.

Заключение

Работа с многопоточностью в Node.js требует осторожности, особенно когда несколько потоков используют общие ресурсы. На данный момент только механизм Atomics помогает эффективно справиться с этой задачей с помощью атомарных операций, которые исключают возникновение состояния гонки.

***

С какими сложностями в многопоточном программировании вы сталкивались, и как решали эти проблемы до знакомства с Atomics?

***

Статья по теме

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...