Состояние гонки в Node.js: практическое руководство

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

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

Что такое состояние гонки?

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

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

  • Поток 1 проверяет, существует ли файл (true), и удаляет его.
  • Поток 2 проверяет, существует ли файл (false), и пытается его открыть.
  • Поток 2 не может открыть файл и выбрасывает ошибку.
  • Поток 2 пытается переименовать файл, что заканчивается сбоем.

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

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

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

Почему возникает состояние гонки?

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

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

Почему в Node.js не предусмотрена защита от состояния гонки?

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

Взгляните на следующий код:

type Database = {
getCounter: () => Promise<number>;
setCounter: (value: number) => Promise<void>;
};

let counter: number = 0;

async function increment(): Promise<void> {
counter = await db.getCounter();
counter++;
await db.setCounter(counter);
}

async function decrement(): Promise<void> {
counter = await db.getCounter();
counter--;
await db.setCounter(counter);
}

// Одновременный вызов функций
increment();
decrement();

// Ожидаемое значение счетчика: 0
// Фактическое значение счетчика может быть 0, 1 или -1, в зависимости от порядка выполнения

Я написал этот код, чтобы показать, как асинхронное программирование может привести к неожиданным результатам, когда вы имеете дело с общими переменными. Я определил тип Database, у которого есть два асинхронных метода для получения и установки значения счетчика в базе данных. Я также объявил две асинхронные функции, increment и decrement, которые обновляют переменную счетчика путем добавления или вычитания единицы. Затем я вызвал эти функции одновременно, не дожидаясь разрешения их промисов. Это создало гонку, при которой конечное значение счетчика зависит от порядка выполнения функций. Код показывает, что вместо ожидаемого 0 счетчик может иметь значение 0, 1 или -1.

Почему вообще в Node.js возможно состояние гонки?

Копнем немного глубже.

Все дело в том, что Node.js является однопоточной системой на уровне приложения. Но на системном уровне Node.js использует библиотеку libuv и механизм цикла событий для обработки асинхронных операций (например, ввод-вывод, функционирование сети или таймеров), и это похоже на использование нескольких потоков. libuv обеспечивает низкоуровневый доступ к операционной системе. В libuv используется цикл событий, который постоянно проверяет наличие событий, таких как входящие запросы, завершенные задачи или события таймера, и выполняет соответствующие обратные вызовы, зарегистрированные для этих событий. Цикл событий работает в главном потоке  —  в том же, где выполняется код JavaScript.

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

Заключение

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ali Aghapour: Race Conditions in Node.js: A Practical Guide

Предыдущая статьяПроект инженерии данных с DAG Airflow «от и до». Часть 2
Следующая статьяСоздание языковой модели для чатов