Как мне избежать состояния гонки с Node.js процесс.отправить?

#node.js

#node.js

Вопрос:

Что именно происходит, когда дочерний процесс (созданный child_process.fork() ) в Node отправляет сообщение своему родителю ( process.send() ) до того, как у родителя есть обработчик событий для message ( child.on("message",...) )? (По крайней мере, кажется, что должен быть какой-то буфер.)

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

Рассмотрим следующий пример кода:

parent.js:

 const child_process = require("child_process");
const child_module = require.resolve("./child");

const run = async () => {
  console.log("parent start");
  const child = child_process.fork(child_module);
  await new Promise(resolve => setTimeout(resolve, 40));
  console.log("add handler");
  child.on("message", (m) => console.log("parent receive:", m));
  console.log("parent end");
};

run();
 

child.js:

 console.log("child start");
process.send("123abc");
console.log("child end");
 

В приведенном выше примере я надеюсь смоделировать «плохое чередование», запретив установку обработчика сообщений на несколько миллисекунд (предположим, что переключение контекста происходит сразу после форка, и что некоторые другие процессы выполняются некоторое время до родительского процесса). node.js процесс может быть запланирован снова). В моем собственном тестировании родительский сервер, похоже, «надежно» получает сообщение с числами << 40 мс (например, 20 мс), но для значений> 35 мс это в лучшем случае ненадежно, а для значений>> 40 мс (например, 50 или 60) сообщение никогда не принимается. Что особенного в этих числах — насколько быстро процессы планируются на моей машине?

Кажется, это не зависит от того, установлен ли обработчик до или после отправки сообщения. Например, я наблюдал оба следующих выполнения с таймаутом, установленным на 40 миллисекунд. Обратите внимание, что в каждом из них дочернее сообщение «end» (указывающее, что process.send() уже произошло) появляется перед «добавить обработчик». В одном случае сообщение получено, но в следующем оно потеряно. Я полагаю, возможно, что буферизация стандартных выходных данных этих процессов потенциально может привести к тому, что эти выходные данные будут искажать истинное выполнение — это то, что здесь происходит?

 Execution A:
  parent start
  child start
  child end
  add handler
  parent end
  parent receive: 123abc
 
 Execution B:
  parent start
  child start
  child end
  add handler
  parent end
 

Короче говоря, есть ли решение этого очевидного состояния гонки? Кажется, я могу «надежно» получать сообщения до тех пор, пока я устанавливаю обработчик достаточно «скоро», но мне просто повезло, или есть какая-то гарантия, которую я получаю? Как мне гарантировать, не полагаясь на удачу, что этот код всегда будет работать (за исключением космических лучей, пролитого кофе и т. Д.)? Кажется, я не могу найти никаких подробностей о том, как это должно работать в документации узла.

Ответ №1:

Что именно происходит, когда дочерний процесс (созданный child_process.fork()) в узле отправляет сообщение своему родителю (process.send()) до того, как у родителя появится обработчик событий для сообщения (child.on(«message»,…))? (По крайней мере, кажется, что должен быть какой-то буфер.)

Во-первых, тот факт, что сообщение, поступившее от другого процесса, попадает в очередь событий nodejs. Он не будет обработан до тех пор, пока текущий код nodejs не завершит все, что он делал, и не вернет управление обратно в цикл событий, чтобы он мог обработать следующее событие в очереди событий. Если этот момент наступает до того, как появится какой-либо прослушиватель для этого входящего события, то оно просто принимается, а затем отбрасывается. Приходит сообщение, код ищет вызов любых зарегистрированных обработчиков событий, и если таковых нет, то все готово. Это то же самое, как если бы вы звонили eventEmitter.emit("someMsg", data) , а слушателей не "someMsg" было. Но, читайте дальше, есть надежда на вашу конкретную ситуацию.

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

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

Если вы сделаете что-то подобное этому:

 const child = child_process.fork(child_module);
child.on("message", (m) => console.log("parent receive:", m));    
 

Тогда вам гарантируется, что ваш обработчик сообщений будет установлен до того, как появится какая-либо вероятность обработки входящего сообщения, и вы не пропустите его. Это происходит потому, что интерпретатор занят выполнением этих двух строк кода и не возвращает управление обратно в цикл событий до тех пор, пока не будут выполнены эти две строки кода. Следовательно, никакое входящее сообщение от child_module не может быть обработано до установки вашего child.on(...) обработчика.


Теперь, если вы намеренно вернетесь обратно к циклу событий, как вы делаете здесь, с await помощью обработчика событий перед установкой, подобного вашему коду здесь:

 const run = async () => {
  console.log("parent start");

  const child = child_process.fork(child_module);

  // this await allows events in the event queue to be processed
  // while this function is suspended waiting for the await
  await new Promise(resolve => setTimeout(resolve, 40));

  console.log("add handler");
  child.on("message", (m) => console.log("parent receive:", m));
  console.log("parent end");
};

run();
 

Затем вы намеренно ввели условие гонки с вашим собственным кодированием, которого можно избежать, просто установив обработчик событий ПЕРЕД await подобным:

 const run = async () => {
  console.log("parent start");

  // no events will be processed before these next three statements run
  const child = child_process.fork(child_module);
  console.log("add handler");
  child.on("message", (m) => console.log("parent receive:", m));

  await new Promise(resolve => setTimeout(resolve, 40));

  console.log("parent end");
};

run();
 

Комментарии:

1. Я вижу — итак, мое состояние гонки является симптомом конкретного способа, который я выбрал, чтобы замедлить моего родителя — ожидание передало управление обратно в цикл событий, который затем имел возможность получить и отбросить сообщение. Если бы я вместо этого выбрал spin-wait с циклом while (или что-то подобное), тогда у меня не было бы проблем. Это точно?

2. @AndrewGies — Это верно, хотя я бы никогда, никогда не рекомендовал ожидание вращения в nodejs, кроме как в качестве своего рода временного тестового примера.