Источник: Nuances of Programming
Состояние гонки — распространенная и сложная проблема в параллельном программировании. Они могут приводить к неожиданным и противоречивым результатам, их сложно отлаживать и исправлять.
В этой статье мы затронем следующие темы: что такое состояние гонки; что происходит с программой, когда оно возникает; почему 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, поскольку она может создать ситуацию, когда порядок событий и обратных вызовов не является определенным, то есть может меняться в зависимости от времени функционирования и планирования потоков.
Заключение
Состояние гонки может привести к неправильным или противоречивым результатам, непредсказуемому или ненадежному поведению, а также уязвимости программы. Поэтому для обеспечения надежности и согласованности приложений используют соответствующие механизмы синхронизации или координации, чтобы избежать гонок, такие как блокировки, семафоры и очереди.
Читайте также:
Перевод статьи Ali Aghapour: Race Conditions in Node.js: A Practical Guide