Node-sync — псевдо-синхронное программирование на nodejs с использованием fibers: различия между версиями

Материал из support.qbpro.ru
imported>Vix
(Новая страница: «http://habrahabr.ru/post/116124/ Надавно была опубликована библиотека node-fibers, вносящая в nodejs и v8 поддер…»)
 
Нет описания правки
 
Строка 1: Строка 1:
http://habrahabr.ru/post/116124/
Надавно была опубликована библиотека node-fibers, вносящая в nodejs и v8 поддержку замечательного fiber/coroutine — тоесть, возможность использовать yield.
Надавно была опубликована библиотека node-fibers, вносящая в nodejs и v8 поддержку замечательного fiber/coroutine — тоесть, возможность использовать yield.
Параллельно, на nodejs groups прошел целый ряд обсуждений на тему всевозможных вариантов упрощения асинхронного синтаксиса.
Параллельно, на nodejs groups прошел целый ряд обсуждений на тему всевозможных вариантов упрощения асинхронного синтаксиса.
Строка 241: Строка 239:


Вам так же могут быть интересны другие https://github.com/lm1/node-fiberize библиотеки https://github.com/lm1/node-fibers-promise, основанные на node-fibers.
Вам так же могут быть интересны другие https://github.com/lm1/node-fiberize библиотеки https://github.com/lm1/node-fibers-promise, основанные на node-fibers.
ИСТОЧНИКИ:
<hr>
* [http://habrahabr.ru/post/116124/ node-sync — псевдо-синхронное программирование на nodejs с использованием fibers]

Текущая версия от 20:40, 24 июля 2024

Надавно была опубликована библиотека 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 решает для меня три важных вопроса:

  1. Освобождение от бесконечной индентации с коллбэками (избавляет от «спагетти-кода»)
  2. Корректная обработка ошибок
  3. Интеграция с существующим кодом/библиотеками без необходимости рефакторинга

Уже больше месяца я использую эту 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.

ИСТОЧНИКИ: