Node-sync — псевдо-синхронное программирование на nodejs с использованием fibers
Надавно была опубликована библиотека node-fibers, вносящая в nodejs и v8 поддержку замечательного fiber/coroutine — тоесть, возможность использовать yield. Параллельно, на nodejs groups прошел целый ряд обсуждений на тему всевозможных вариантов упрощения асинхронного синтаксиса.
Вдохновившись возможностями, которые дают «волокна», я написал библиотеку node-sync, которая делает разработку в асинхронном окружении nodejs значительно удобнее, а код — нагляднее.
Синопсис
// Обычная асинхронная функция, вызывает callback с результатом через 1 сек function someAsyncFunction(a, b, callback) { setTimeout(function(){ callback(null, a + b); }, 1000) } // Вызываем эту функцию синхронно, используя Function.prototype.sync(), // работающий по тому же принципу, что и call() // на этом моменте текуший поток "зависнет" на секунду, пока функция не вернет значение var result = someAsyncFunction.sync(null, 2, 3); console.log(result); // "5" через 1 секунду
Философия
Главная идея в том, что метод Function.prototype.sync() встроен в любую функцию по умолчанию, а так же, его интерфейс соответствует всем давно известному call(). Подключив библиотеку sync, мы можем вызвать любую асинхронную функцию синхронно, без написания дополнительного кода. Псевдо-синхронное программирование — потому, что фактически, Function.prototype.sync() не блокирует весь процесс, а только текущий поток. Тело самой функции выполняется асинхронно, мы просто дожидаемся результата (используя «yield»). Но при этом, код читается «синхронно».
node-sync решает для меня три важных вопроса:
- Освобождение от бесконечной индентации с коллбэками (избавляет от «спагетти-кода»)
- Корректная обработка ошибок
- Интеграция с существующим кодом/библиотеками без необходимости рефакторинга
Уже больше месяца я использую эту node-sync в своем приложении, переход был незаметным — я просто начал писать новый код на «псевдо-синхронный» манер, старый код остался прежним.
Блокировка
Прелесть «волокон» состоит в том, что при ожидании (yield) блокируется только текущий поток, а не весь процесс. Наглядный пример блокировки всего процесса — fs.readFileSync и другие псевдо-синхронные функции.
Используя «волокна» можно избежать глобальной блокировки и, при этом, прочитать файл синхронно:
var fs = require('fs'), Sync = require('sync'); // запускаем новый поток Sync(function(){ // тело потока --> // синхронно читаем файл, используя Function.prototype.sync() var source = fs.readFile.sync(null, __filename); // выводим содержимое текущего файла console.log(String(source)); })
Отличие этого кода в том, что пока мы ожидаем ответ от fs.readFile.sync(), приложение спокойно продолжает выполнять другие операции.
Обработка ошибок
Всем, кто хоть раз пробовал написать на nodejs что-то серьезнее, чем «hello world app», наверняка знакома рутина с обработкой ошибок. Если следовать официальному дизайну callback-функций, первым агрументом всегда должна быть возвращена ошибка. При этом, использовать throw в асинхронном окружении чревато падением всего event-loop.
Весьма реальный код на nodejs с корректной обработкой ошибок:
// Функция должна что-то сделать и вернуть результат, // при этом, корректно обработать ошибку в случае неудачи function asyncFunction(callback) { var p_client = new Db('test', new Server("127.0.0.1", 27017, {})); p_client.open(function(err, p_client) { if (err) return callback(err); // <-- рутина p_client.createCollection('test_custom_key', function(err, collection) { if (err) return callback(err); // <-- рутина collection.insert({'a':1}, function(err, docs) { if (err) return callback(err); // <-- рутина collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) { if (err) return callback(err); // <-- рутина cursor.toArray(function(err, items) { if (err) return callback(err); // <-- рутина // результат = items callback(null, items); }); }); }); }); }); }
Такая-же функция, только использующая sync. Результат ее работы идентичен функции выше, учитывая обработку ошибок. Если какая-то из вызываемых функций вернет ошибку в авто-callback, который передает ей sync(), то эта ошибка прозрачно попадет в результирующий callback, который мы указали потоку вторым аргументом.
function syncFunction(callback) { // запускаем поток Sync(function(){ var p_client = new Db('test', new Server("127.0.0.1", 27017, {})); p_client.open.sync(p_client); var collection = p_client.createCollection.sync(p_client, 'test'); collection.insert.sync(collection, {'a' : 1}); var cursor = collection.find.sync(collection, {'_id':new ObjectID("aaaaaaaaaaaa")) var items = cursor.toArray.sync(cursor); // результат = items return items; }, callback) // <-- результат возвращаем в callback }
Используя специальный метод Function.prototype.async(), эта функция может быть еще проще (работает аналогично функции выше):
var syncFunction = function() { var p_client = new Db('test', new Server("127.0.0.1", 27017, {})); p_client.open.sync(p_client); var collection = p_client.createCollection.sync(p_client, 'test'); collection.insert.sync(collection, {'a' : 1}); var cursor = collection.find.sync(collection, {'_id':new ObjectID("aaaaaaaaaaaa")) var items = cursor.toArray.sync(cursor); // результат = items return items; }.async() // <-- в этом месте мы превращаем ее в асинхронную функцию
Параллельность
Иногда, нам нужно выполнить несколько функций параллельно, при этом, дождаться всех результатов, и только тогда продолжить. Для этого существует Sync.Parallel:
var Sync = require('sync'); // Какая-то асинхронная функция, возвращает результат через секунду function someAsyncFunction(a, b, callback) { setTimeout(function(){ callback(null, a + b); }, 1000) } // Новый поток Sync(function(){ // Запускаем параллельно функцию с разными аргументами // поток будет заблокирован до тех пор, пока не будут возвращены оба результата var results = Sync.Parallel(function(callback){ someAsyncFunction(2, 2, callback()); someAsyncFunction(5, 5, callback()); }); console.log(results); // [ 4, 10 ] // Ассоциативный вариант var results = Sync.Parallel(function(callback){ someAsyncFunction(2, 2, callback('foo')); // assign the result to 'foo' someAsyncFunction(5, 5, callback('bar')); // assign the result to 'bar' }); console.log(results); // { foo: 4, bar: 10 } })
На днях общался с господином laverdet (создатель node-fibers для v8), и он предложил весьма интересную парадигму «будущего». Я добавил новый метод Function.prototype.future() — его тоже можно использовать для параллельности:
// Новый поток Sync(function(){ // Запускаем someAsyncFunction, но не блокируем поток var foo = someAsyncFunction.future(null, 2, 2); var bar = someAsyncFunction.future(null, 5, 5); // foo, bar - это билеты в будущее console.log(foo); // { [Function: Future] result: [Getter], error: [Getter] } // А вот теперь, дожидаемся значений от foo и bar console.log(foo.result, bar.result); // 4 10 - ровно через секунду (не две) })
Установка
$ npm install sync
$ node-fibers my_file.js
Имейте ввиду, что для поддержки fibers вам нужно использовать скрипт «node-fibers» вместо «node».
API
var Sync = require('sync'); // Новый поток, fn - функция-тело, результат не возвращается Sync(fn) // Новый поток, fn - функция-тело, результат/ошибка возвращается в callback Sync(fn, callback) // внутри используется инкрементальный callback() Sync.Parallel(function(callback){ callback() // без аргумента (вернется массив) callback('foo') // или ассоциативный ключ }) // Вызывает функцию асинхронно и ждет результата/ошибки // obj - контекст, остальные аргументы попадут по порядку в функцию Function.prototype.sync(obj, arg1, arg2) // Вызывает функцию асинхронно и не ждет результата, возвращая управление в контекст // возвращает объект/функцию Future, у которой есть getter 'result' // при попытке получения Future.result, поток будет заблокирован до тех пор, пока результат не будет получен // obj - контекст, остальные аргументы попадут по порядку в функцию Function.prototype.future(obj, arg1, arg2) // Делает из любой синхронной функции - асинхронную // возвращает функцию, которую можно вызвать асинхронно // obj - контекст Function.prototype.async(obj)
Резюме
Я и дальше намерен развивать это направление в nodejs, ибо мне оно кажется очень правильным. Буду рад, если кто-то из вас вдохновится этой идеей и внесет свой вклад в ее развитие.
Советую посмотреть довольно подробные примеры использования библиотеки. Если вы намерены поучавствовать в разработке — форкайте, милости просим, только не забывайте про тесты. Я так же добавил скрипт в benchmarks. Если у кого-то появятся еще идеи, как можно протестировать скорость работы fibers, будет круто.
Хочу поблагодарить egorF за брейнсторминг, и за то, что вообще заразил меня темой fibers :)
Вам так же могут быть интересны другие https://github.com/lm1/node-fiberize библиотеки https://github.com/lm1/node-fibers-promise, основанные на node-fibers.
ИСТОЧНИКИ: