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

Например, рассмотрим сценарий, в котором два человека, Алиса и Боб, одновременно пытаются снять деньги с общего банковского счета. Если Алиса проверяет баланс и видит, что денег достаточно для снятия, она снимает 100 долларов. В то же время Боб также проверяет баланс и видит, что денег достаточно для вывода, поэтому он также снимает 100 долларов. Конечным результатом является то, что баланс счета уменьшился на 200 долларов вместо 100 долларов, что неверно. Это пример состояния гонки, потому что результат действия зависит от времени доступа к общему ресурсу (банковскому счету).

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

Давайте посмотрим на реальный пример состояния гонки в приложении Typescript. В этом примере мы создадим простую программу, которая увеличивает счетчик и выводит окончательный результат.

let counter = 0;

async function incrementCounter() {
  counter++;
  console.log(counter);
}

async function run() {
  for (let i = 0; i < 10; i++) {
    incrementCounter();
  }
}

run();

Когда мы запускаем этот код, мы ожидаем, что счетчик будет увеличен в 10 раз, а окончательный результат будет 10. Однако, поскольку функция incrementCounter не синхронизирована, окончательный результат может быть неожиданным и может не равняться 10. Это связано с тем, что несколько вызовов к функции incrementCounter может происходить одновременно, в результате чего несколько потоков пытаются увеличить счетчик одновременно. Чтобы избежать этого состояния гонки, мы можем использовать такой механизм, как блокировка, для синхронизации доступа к счетчику.

let counter = 0;
let lock = false;

async function incrementCounter() {
  while (lock) {
    // Wait until the lock is released
  }

  lock = true;
  counter++;
  console.log(counter);
  lock = false;
}

async function run() {
  for (let i = 0; i < 10; i++) {
    incrementCounter();
  }
}

run();

В этой обновленной версии кода переменная блокировки действует как семафор, чтобы гарантировать, что только один поток может получить доступ к счетчику за раз. Перед увеличением счетчика функция проверяет значение блокировки. Если для него установлено значение true, функция ожидает, пока для него не будет установлено значение false. Как только блокировка получена, счетчик увеличивается, результат регистрируется, и блокировка снимается. Это гарантирует, что счетчик увеличивается предсказуемым и контролируемым образом, избегая состояния гонки.

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