Асинхронное Программирование, Обратные Вызовы И Использованиеprocess.nexttick()

Хотя большинство студентов Node.js имеют некоторые знания JavaScript и опыт его использования в контексте браузера, при обсуждении практических вопросов многим сложно понять работу стандартной библиотеки и механизмы обеспечения асинхронного выполнения кода, содержащего множество вложенных кодов.

.

обратные вызовы.

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

Думаю, статья будет полезна и тем, кто занимается написанием продуктивных фреймворков для браузеров.



Лирическое отступление: цикл событий, лежащий в основе Node.js

Как уже много раз писалось, в основе Node.js лежит цикл событий, реализованный библиотекой.

Либев .

На каждой итерации цикла происходит следующее: в первую очередь выполняются функции, установленные на предыдущей итерации цикла с помощью процесса.

nextTick().

Далее идет обработка событий libev, в частности событий таймера.

И последнее, но не менее важное: опрос.

Либейо для завершения операций ввода-вывода и выполнения установленных для них обратных вызовов.

Если при прохождении цикла окажется, что с помощьюprocess.nextTick() не установлена никакая функция, нет таймера и очереди запросов в libev и libeio пусты, то узел завершает работу.

Если вы хотите узнать больше о том, как работает цикл событий, я рекомендую вам пролистать презентацию.

www.slideshare.net/jacekbecela/introduction-to-nodejs .

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

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

Рассмотрим пример HTTP-сервера: при обращении к нему он считывает из текущей папки файл с именем, соответствующим строке запроса, и возвращает некий хэш его содержимого.



Синхронная версия тестового сервера

  
  
  
  
   

// readFileSync.js var http = require('http'), fs = require('fs'); function func1(str) { var res = ''; for (var i = 0, l = str.length; i < l; i++) { res += str.charCodeAt(i); } return res; } function func2(str) { var res = 0; for (var i = 0, l = str.length; i < l; i++) { res += Math.sin(str.charCodeAt(i)); } return '' + res; } http.createServer(function (req, res) { // Very simple and dangerous check var filename = req.url.replace(/\?.

*/, '').

replace(/(\.

\.

|\/)/, ''); // Read file from disk try { var filecontent = fs.readFileSync(filename, 'utf8'); } catch (e) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('File ' + filename + ' doesn\'t exist'); return; } // Calculate checksum var hash = func2(func1(filecontent)); // Write response res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(hash); }).

listen(8124, "127.0.0.1");

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

Более того, если чтение занимает T читать секунд, и вычисление суммы T расчет секунд, то такой блокирующий сервер сможет обслуживать менее 1/(T читать + Т расчет ) запросов в секунду.

Как мы можем улучшить наш сервер, разрешив ему обрабатывать больше соединений? Прежде всего используйте неблокирующее чтение файлов.



Асинхронное чтение файла и попытка использовать обратные вызовы



// readFile.js var http = require('http'), fs = require('fs'); function func1(str) { var res = ''; for (var i = 0, l = str.length; i < l; i++) { res += str.charCodeAt(i); } return res; } function func2(str) { var res = 0; for (var i = 0, l = str.length; i < l; i++) { res += Math.sin(str.charCodeAt(i)); } return '' + res; } http.createServer(function (req, res) { // Very simple and dangerous check var filename = req.url.replace(/\?.

*/, '').

replace(/(\.

\.

|\/)/, ''); // Read file from disk fs.readFile(filename, 'utf8', function (err, filecontent) { if (err) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('File ' + filename + ' doesn\'t exist'); return; } // Calculate checksum var hash = func2(func1(filecontent)); // Write response res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(hash); }); }).

listen(8124, "127.0.0.1");

> Используя асинхронное чтение, мы можем добиться более быстрой обработки каждого запроса за счет того, что во время вычислений в фоновом режиме файл будет читаться для другого запроса.

читать , Т расчет ), а не (Т читать + Т расчет ), как и в случае с синхронным сервером.

Таким образом, при поступлении двух запросов в синхронном случае время их обслуживания будет равно T читать1 + Т расчет1 + Т читать2 + Т расчет2 , а при асинхронном чтении может достигать T читать1 + Т читать2 +мин(Т расчет1 , Т расчет2 ).

Это уже хорошо.

Но что, если время обработки файла намного превышает время чтения файла и к тому же сильно колеблется? В этом случае за время расчета суммы для одного файла могут успеть прочитаться несколько других файлов меньшего размера, которые затем будут быстро обработаны.

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

Однако логично захотеть вернуть результат раньше клиентам, которые запрашивают файлы меньшего размера или файлы, требующие меньшего времени обработки.

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

.

Благодаря этому в промежутке между расчетом func1() и func2() для одного запроса может быть принято новое соединение и создана задача на чтение другого файла, либо может быть считан файл меньшего размера, который уже был прочитан.

обработанный.

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

в том месте, где он вызывается.



// readFile-and-sync-chain.js var http = require('http'), fs = require('fs'); function func1(str) { var res = ''; for (var i = 0, l = str.length; i < l; i++) { res += str.charCodeAt(i); } return res; } function func2(str) { var res = 0; for (var i = 0, l = str.length; i < l; i++) { res += Math.sin(str.charCodeAt(i)); } return '' + res; } function func1_cb(str, cb) { var res = func1(str); cb(res); } function func2_cb(str, cb) { var res = func2(str); cb(res); } http.createServer(function (req, res) { // Very simple and dangerous check var filename = req.url.replace(/\?.

*/, '').

replace(/(\.

\.

|\/)/, ''); // Read file from disk fs.readFile(filename, 'utf8', function (err, filecontent) { if (err) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('File ' + filename + ' doesn\'t exist'); return; } // Calculate checksum func1_cb(filecontent, function (str) { func2_cb(str, function (hash) { // Write response res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(hash); }); }); }); }).

listen(8124, "127.0.0.1");

Что же произойдет на самом деле? Никакого волшебства, конечно, нет. Единственная разница будет заключаться в том, что мы заменили чисто императивный код расчета сумм на код с двумя вложенными обратными функциями, которые будут последовательно вызывать друг друга и немного увеличивать время расчета сумм из-за ненужных вызовов функций, что в конечном итоге только ухудшит результат. производительность нашего сервера.



Асинхронное чтение файлов и правильный асинхронная обработка

Чтобы передать управление основному потоку выполнения и одновременно поручить будущей задаче дальнейшей обработки суммы после вычисления func1(), можно использовать старый проверенный инструмент, доступный в JavaScript: setTimeout(fn, 0) .

Это именно та функция, которую мы бы использовали, если бы программировали для браузеров.

Но, как я писал выше, в Node.js есть функцияprocess.nextTick(fn), которая более эффективна и переданная ей функция гарантированно будет выполнена раньше, чем функции, заданные с помощью таймеров или являющиеся обработчиками событий из сокетов или файловая система.

Таким образом, код сервера readFile-and-sync-chain.js можно переписать следующим образом:

// readFile-and-nextTick.js var http = require('http'), fs = require('fs'); function func1(str) { var res = ''; for (var i = 0, l = str.length; i < l; i++) { res += str.charCodeAt(i); } return res; } function func2(str) { var res = 0; for (var i = 0, l = str.length; i < l; i++) { res += Math.sin(str.charCodeAt(i)); } return '' + res; } function func1_cb(str, cb) { var res = func1(str); process.nextTick(function () { cb(res); }); } function func2_cb(str, cb) { var res = func2(str); process.nextTick(function () { cb(res); }); } http.createServer(function (req, res) { // Very simple and dangerous check var filename = req.url.replace(/\?.

*/, '').

replace(/(\.

\.

|\/)/, ''); // Read file from disk fs.readFile(filename, 'utf8', function (err, filecontent) { if (err) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('File ' + filename + ' doesn\'t exist'); return; } // Calculate checksum func1_cb(filecontent, function (str) { func2_cb(str, function (hash) { // Write response res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(hash); }); }); }); }).

listen(8124, "127.0.0.1");

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



Сравнение производительности рассмотренных вариантов

Все, что сказано выше, — это, по большей части, рассуждения о правильной архитектуре.

Фактически производительность того или иного варианта может зависеть от размера читаемого файла и времени его обработки, от нелинейности зависимости времени обработки от размера файла, а также от того, насколько разноплановые запросы обрабатывает сервер.

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

Для сравнения использовались файлы размером от 128 байт до 1 МБ и загружался сервер с помощью Apache Bench:

ab2 -n 1000 -c 100 http://127.0.0.1:8124/filename

Результаты показаны на графиках:

Асинхронное программирование, обратные вызовы и использованиеprocess.nextTick()

Как видите, использование асинхронного чтения фактически повышает производительность сервера для файлов большого размера.

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

Как и ожидалось, использование обычных вложенных функций не повышает производительность.

Но использование асинхронных обратных вызовов при пошаговой обработке файла принесло результаты, хорошо заметные для файлов небольшого размера.

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

Надеюсь, у меня будет время провести обширное тестирование с несколькими файлами и различным количеством запросов к ним.

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

P.S. Спасибо новичкам nodejs forum.nodejs.ru за поднятие этого вопроса.

Теги: #node.js #async #aio #benchmark #JavaScript #nextTick #node.js

Вместе с данным постом часто просматривают: