Правильный способ написать неблокирующую функцию в Node.js

#javascript #asynchronous #vue.js #promise

#javascript #node.js #обещаю

Вопрос:

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

 function longRunningFunc(val, mod) {
    return new Promise((resolve, reject) => {
        sum = 0;
        for (var i = 0; i < 100000; i  ) {
            for (var j = 0; j < val; j  ) {
                sum  = i   j % mod
            }
        }
        resolve(sum)
    })
}

console.log("before")
longRunningFunc(1000, 3).then((res) => {
    console.log("Result: "   res)
})
console.log("after")
  

Результат выглядит как ожидаемый:

 before     // delay before printing below lines
after
Result: 5000049900000
  

Но программа ожидает перед печатью второй и третьей строк. Можете ли вы объяснить, каким должен быть правильный способ сначала напечатать «до» и «после», а затем (через некоторое время) результат?

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

1. Что ж, если вы хотите действительно протестировать «через некоторое время», вы могли бы поместить setTimeout вокруг вашего оператора resolve (sum).

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

3. По теме: На стороне клиента также существует Web Worker API для создания отдельных потоковых задач.

Ответ №1:

Обертывание кода в promise (как вы это сделали) не делает его неблокирующим. Функция-исполнитель обещаний (обратный вызов, который вы передаете new Promise(fn) , вызывается синхронно и блокируется, поэтому вы видите задержку в получении выходных данных.

На самом деле, нет способа создать свой собственный простой Javascript-код (подобный тому, что у вас есть), который неблокируется, кроме как поместить его в дочерний процесс, используя WorkerThread, используя какую-либо стороннюю библиотеку, которая создает новые потоки Javascript, или используя новый экспериментальный node.js API для потоков. Обычный node.js запускает ваш Javascript как блокирующий и однопоточный, независимо от того, заключен он в обещание или нет.

Вы можете использовать такие вещи, как setTimeout() , чтобы изменить «когда» ваш код выполняется, но всякий раз, когда он запускается, он все равно будет блокироваться (как только он начинает выполняться, ничто другое не может запускаться, пока это не будет сделано). Асинхронные операции в node.js все библиотеки используют некоторую форму базового машинного кода, который позволяет им быть асинхронными (или они просто используют другие node.js асинхронные API, которые сами используют реализации собственного кода).

Но программа ожидает перед печатью второй и третьей строк. Можете ли вы объяснить, каким должен быть правильный способ сначала напечатать «до» и «после», а затем (через некоторое время) результат?

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

Итак, вы могли бы сделать это:

 function longRunningFunc(val, mod) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            sum = 0;
            for (var i = 0; i < 100000; i  ) {
                for (var j = 0; j < val; j  ) {
                    sum  = i   j % mod
                }
            }
            resolve(sum)
        }, 10);
    })
}
  

Это привело бы к перепланированию отнимающего много времени for цикла для запуска позже и могло бы «показаться», что он неблокирующий, но на самом деле он все еще блокируется — он просто запускается позже. Чтобы сделать ее действительно неблокирующей, вам пришлось бы использовать один из методов, упомянутых ранее, чтобы вывести ее из основного потока Javascript.

Способы создания фактического неблокирующего кода в node.js:

  1. Запустите ее в отдельном дочернем процессе и получите асинхронное уведомление, когда это будет сделано.
  2. Используйте новые экспериментальные рабочие потоки в node.js v11
  3. Напишите свое собственное дополнение к машинному коду для node.js и используйте потоки libuv или потоки уровня ОС в вашей реализации (или другие асинхронные инструменты уровня ОС).
  4. Создавайте поверх ранее существующих асинхронных API и не используйте свой собственный код, который занимает очень много времени в основном потоке.

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

1. По моему опыту, для потоков уровня операционной системы в аддонах C libuv был довольно хорош. Вот базовый пример, который я нашел . Я лично использовал его для обработки изображений в реальном времени, чтобы выполнить обнаружение объекта в неблокирующем потоке, а затем Node.js распределил обнаруженные центроиды по подключенным TCP-клиентам (которые использовали данные для приведения в действие двигателей для перемещения к обнаруженному объекту).

Ответ №2:

Функция-исполнитель обещания выполняется синхронно, и именно поэтому ваш код блокирует основной поток выполнения.

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

Вы можете либо написать свой собственный асинхронный цикл, используя promise API, либо использовать асинхронную функцию. Асинхронные функции позволяют приостанавливать и возобновлять функции (повторный вход) и скрывают от вас большую часть сложности.

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

Наконец: реальное решение проблемы такого рода, вероятно, является Worker.

 const $ = document.querySelector.bind(document)
const BIG_NUMBER = 1000
let count = 0

// Note that this could also use requestIdleCallback or requestAnimationFrame
const tick = (fn) => new Promise((resolve) => setTimeout(() => resolve(fn), 5))

async function longRunningTask(){
    while (count   < BIG_NUMBER) await tick()
    console.log(`A big number of loops done.`)
}

console.log(`*** STARTING ***`)
longRunningTask().then(() => console.log(`*** COMPLETED ***`))
$('button').onclick = () => $('#output').innerHTML  = `Current count is: ${count}<br/>`  
 * {
  font-size: 16pt;
  color: gray;
  padding: 15px;
}  
 <button>Click me to see that the UI is still responsive.</button>
<div id="output"></div>