Как обернуть вызовы асинхронных функций в функцию синхронизации в Node.js или Javascript?

122

Предположим, вы поддерживаете библиотеку, которая предоставляет функцию getData. Ваши пользователи называют его , чтобы получить фактические данные:
var output = getData();
Под данные Колпак сохраняются в файле , так что вы реализованы с getDataпомощью Node.js встроенной fs.readFileSync. Очевидно, что обе функции getDataи fs.readFileSyncявляются функциями синхронизации. Однажды вам сказали переключить базовый источник данных на репо, например MongoDB, к которому можно получить доступ только асинхронно. Вам также сказали, чтобы вы не злили своих пользователей, getDataAPI нельзя изменить так, чтобы он возвращал просто обещание или запрашивал параметр обратного вызова. Как вы соответствуете обоим требованиям?

Асинхронная функция с использованием обратного вызова / обещания - это ДНК JavasSript и Node.js. Любое нетривиальное JS-приложение, вероятно, пронизано этим стилем кодирования. Но такая практика легко может привести к так называемой пирамиде гибели обратных вызовов. Хуже того, если какой-либо код в любом вызывающем абоненте в цепочке вызовов зависит от результата асинхронной функции, этот код также должен быть заключен в функцию обратного вызова, налагая ограничение стиля кодирования на вызывающего. Время от времени я обнаруживаю необходимость инкапсулировать асинхронную функцию (часто предоставляемую в сторонней библиотеке) в функцию синхронизации, чтобы избежать массового глобального повторного факторинга. Поиск решения по этому поводу обычно заканчивался Node Fibers.или пакеты npm, производные от него. Но Fibers просто не могут решить проблему, с которой я столкнулся. Даже пример, приведенный автором Фиберса, иллюстрирует этот недостаток:

...
Fiber(function() {
    console.log('wait... ' + new Date);
    sleep(1000);
    console.log('ok... ' + new Date);
}).run();
console.log('back in main');

Фактический выход:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
back in main
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)

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

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)
back in main

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

сокр
источник
2
Асинхронные функции никогда нельзя сделать синхронными в Node, и даже если бы это было возможно, вам не следует этого делать. Проблема в том, что в модуле fs можно увидеть совершенно отдельные функции для синхронного и асинхронного доступа к файловой системе. Лучшее, что вы можете сделать, - это замаскировать появление асинхронности с помощью обещаний или сопрограмм (генераторов в ES6). Для управления пирамидами обратного вызова дайте им имена вместо определения в вызове функции и используйте что-то вроде библиотеки async.
qubyte
8
Для dandavis async всплывает детали реализации в цепочку вызовов, иногда вызывая глобальный рефакторинг. Это губительно и даже губительно для сложного приложения, где важны модульность и локализация.
abbr
4
«Пирамида обратного вызова» - это лишь представление проблемы. Promise может скрыть или замаскировать его, но не может решить истинную проблему: если вызывающий объект асинхронной функции зависит от результатов асинхронной функции, он должен использовать обратный вызов, как и его вызывающий объект и т. Д. Это классический пример наложения ограничений на вызывающий абонент просто из-за деталей реализации.
abbr
1
@abbr: Спасибо за модуль deasync, описание вашей проблемы - это именно то, что я искал и не мог найти никаких работоспособных решений. Я возился с генераторами и итерациями, но пришел к тем же выводам, что и вы.
Кевин Джангиани
2
Стоит отметить, что принудительно синхронизировать асинхронную функцию почти никогда не стоит . У вас почти всегда есть лучшее решение, которое сохраняет асинхронность функции неизменной, но при этом обеспечивает тот же эффект (например, последовательность, установка переменных и т. Д.).
Призрак Мадары

Ответы:

105

deasync превращает асинхронную функцию в синхронизацию, реализованную с помощью механизма блокировки, путем вызова цикла событий Node.js на уровне JavaScript. В результате deasync только блокирует выполнение последующего кода, не блокируя весь поток, не вызывая ожидания занятости. С помощью этого модуля вот ответ на вызов jsFiddle:

function AnticipatedSyncFunction(){
  var ret;
  setTimeout(function(){
      ret = "hello";
  },3000);
  while(ret === undefined) {
    require('deasync').runLoopOnce();
  }
  return ret;    
}


var output = AnticipatedSyncFunction();
//expected: output=hello (after waiting for 3 sec)
console.log("output="+output);
//actual: output=hello (after waiting for 3 sec)

(отказ от ответственности: я являюсь соавтором deasync. Модуль был создан после публикации этого вопроса и не нашел работоспособного предложения.)

сокр
источник
Кому-нибудь еще повезло с этим? Я не могу заставить это работать.
Newman
3
Я не могу заставить его работать должным образом. вам следует улучшить свою документацию для этого модуля, если вы хотите, чтобы он использовался чаще. Я сомневаюсь, что авторы точно знают, каковы последствия использования модуля, а если и знают, то уж точно не документируют их.
Alexander Mills
5
Пока есть одна подтвержденная проблема, задокументированная в трекере проблем на github. Проблема исправлена ​​в Node v0.12. Остальное, что я знаю, - это просто беспочвенные предположения, которые не стоит документировать. Если вы считаете, что ваша проблема вызвана деасинхронизацией, опубликуйте автономный дублируемый сценарий, и я изучу его.
abbr
Я попытался использовать его и получил некоторые улучшения в моем сценарии, но мне все равно не повезло с датой. Я изменил код следующим образом: function AnticipatedSyncFunction(){ var ret; setTimeout(function(){ var startdate = new Date() //console.log(startdate) ret = "hello" + startdate; },3000); while(ret === undefined) { require('deasync').runLoopOnce(); } return ret; } var output = AnticipatedSyncFunction(); var startdate = new Date() console.log(startdate) console.log("output="+output); и я ожидаю увидеть разницу в 3 секунды в выводе даты!
Alex
@abbr можно ли просматривать и использовать без зависимости от узла>
Ганди
5

Также есть модуль синхронизации npm. который используется для синхронизации процесса выполнения запроса.

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

Образец кода

/*require sync module*/
var Sync = require('sync');
    app.get('/',function(req,res,next){
      story.find().exec(function(err,data){
        var sync_function_data = find_user.sync(null, {name: "sanjeev"});
          res.send({story:data,user:sync_function_data});
        });
    });


    /*****sync function defined here *******/
    function find_user(req_json, callback) {
        process.nextTick(function () {

            users.find(req_json,function (err,data)
            {
                if (!err) {
                    callback(null, data);
                } else {
                    callback(null, err);
                }
            });
        });
    }

справочная ссылка: https://www.npmjs.com/package/sync

Санджив Кумар
источник
4

Если функция Fiber действительно превращает сон асинхронной функции в синхронизацию

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

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

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

Моя цель - минимизировать влияние на вызывающего абонента при изменении метода сбора данных с синхронного на асинхронный.

И обещания, и волокна могут это сделать.

Берги
источник
1
это АБСОЛЮТНО худшее, что вы можете сделать с Node.js: «синхронно выглядящий код, который использует асинхронные функции, а затем будет выполняться асинхронно». если ваш API сделает это, вы разрушите жизнь. если он асинхронный, он должен требовать обратного вызова и выдавать ошибку, если обратный вызов не предоставляется. это лучший способ создать API, если ваша цель - обмануть людей.
Alexander Mills
@AlexMills: Да, это было бы действительно ужасно . Однако, к счастью, это не то, что API может сделать. Асинхронный API всегда должен принимать обратный вызов / возвращать обещание / ожидать запуска внутри волокна - без него он не работает. Afaik, фибры в основном использовались в быстрых и грязных скриптах, которые блокировали и не имели параллелизма, но хотели использовать асинхронные API; как и в узле, иногда бывают случаи, когда вы использовали бы синхронные fsметоды.
Bergi
2
Мне вообще нравится node. Особенно, если я могу использовать машинописный текст вместо чистого js. Но вся эта асинхронная чушь, которая пронизывает все, что вы делаете, и буквально заражает каждую функцию в цепочке вызовов, как только вы решаете сделать один асинхронный вызов, - это то, что я действительно ... очень ненавижу. Async api похож на инфекционное заболевание: один вызов заражает всю вашу базу кода, вынуждая вас переписать весь имеющийся у вас код. Я действительно не понимаю, как можно утверждать, что это хорошо .
Крис
@Kris Node использует асинхронную модель для задач ввода-вывода, потому что это быстро и просто. Вы также можете делать многие вещи синхронно, но блокировка происходит медленно, поскольку вы не можете ничего делать одновременно - если вы не используете потоки, которые все усложняют.
Берги
@Bergi Я прочитал манифест, поэтому я знаю аргументы. Но изменение существующего кода в асинхронный момент , когда вы попали , что первый вызов апи , который не имеет никакого эквивалента синхронизации не просто. Все ломается, и каждую строчку кода нужно тщательно изучить. Если ваш код не является тривиальным, я гарантирую ... потребуется время, чтобы преобразовать и заставить его снова работать после преобразования всего этого в асинхронную идиому.
Крис
2

Вы должны использовать обещания:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async () => {
    return await asyncOperation();
}

const topDog = () => {
    asyncFunction().then((res) => {
        console.log(res);
    });
}

Мне больше нравятся определения стрелочных функций. Но любую строку вида «() => {...}» можно также записать как «function () {...}»

Таким образом, topDog не является асинхронным, несмотря на вызов асинхронной функции.

введите описание изображения здесь

РЕДАКТИРОВАТЬ: Я понимаю, что очень часто вам нужно обернуть асинхронную функцию внутри функции синхронизации внутри контроллера. Для таких ситуаций есть трюк для вечеринки:

const getDemSweetDataz = (req, res) => {
    (async () => {
        try{
            res.status(200).json(
                await asyncOperation()
            );
        }
        catch(e){
            res.status(500).json(serviceResponse); //or whatever
        }
    })() //So we defined and immediately called this async function.
}

Используя это с обратными вызовами, вы можете сделать обертку, которая не использует обещания:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async (callback) => {
    let res = await asyncOperation();
    callback(res);
}

const topDog = () => {
    let callback = (res) => {
        console.log(res);
    };

    (async () => {
        await asyncFunction(callback)
    })()
}

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

user2485309
источник
1

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

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

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

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

Оборотная сторона: если фреймворк использует setTimeoutили Promiseвнутри себя, он выйдет из контекста волокна. Это можно обойти с помощью насмешливый setTimeout, Promise.thenи все обработчики событий.

Вот как вы можете отдавать волокно, пока Promiseне разрешится. Этот код принимает функцию async (возврат обещания) и возобновляет работу волокна, когда обещание разрешено:

рамочные-entry.js

console.log(require("./my-plugin").run());

асинхронному lib.js

exports.getValueAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Async Value");
    }, 100);
  });
};

мой-plugin.js

const Fiber = require("fibers");

function fiberWaitFor(promiseOrValue) {
  var fiber = Fiber.current, error, value;
  Promise.resolve(promiseOrValue).then(v => {
    error = false;
    value = v;
    fiber.run();
  }, e => {
    error = true;
    value = e;
    fiber.run();
  });
  Fiber.yield();
  if (error) {
    throw value;
  } else {
    return value;
  }
}

const asyncLib = require("./async-lib");

exports.run = () => {
  return fiberWaitFor(asyncLib.getValueAsync());
};

мой-entry.js

require("fibers")(() => {
  require("./framework-entry");
}).run();

При запуске node framework-entry.jsон выдаст сообщение об ошибке: Error: yield() called with no fiber running. Если запустить, node my-entry.jsвсе работает как положено.

Тамас Хегедус
источник
0

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

мы можем синхронизировать его, используя важную функциональность Fiber () Используйте await () и defer (), мы вызываем все методы, используя await (). затем замените функции обратного вызова на defer ().

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

function add (var a, var b, function(err,res){
       console.log(res);
});

 function sub (var res2, var b, function(err,res1){
           console.log(res);
    });

 function div (var res2, var b, function(err,res3){
           console.log(res3);
    });

Синхронизируйте приведенный выше код с помощью Fiber (), await () и defer ()

fiber(function(){
     var obj1 = await(function add(var a, var b,defer()));
     var obj2 = await(function sub(var obj1, var b, defer()));
     var obj3 = await(function sub(var obj2, var b, defer()));

});

Я надеюсь, это поможет. Спасибо

Мохан Рамакришна
источник
0

В настоящее время этот шаблон генератора может быть решением во многих ситуациях.

Вот пример последовательных подсказок консоли в nodejs с использованием функции async readline.question:

var main = (function* () {

  // just import and initialize 'readline' in nodejs
  var r = require('readline')
  var rl = r.createInterface({input: process.stdin, output: process.stdout })

  // magic here, the callback is the iterator.next
  var answerA = yield rl.question('do you want this? ', r=>main.next(r))    

  // and again, in a sync fashion
  var answerB = yield rl.question('are you sure? ', r=>main.next(r))        

  // readline boilerplate
  rl.close()

  console.log(answerA, answerB)

})()  // <-- executed: iterator created from generator
main.next()     // kick off the iterator, 
                // runs until the first 'yield', including rightmost code
                // and waits until another main.next() happens
drodsou
источник
-1

Вы должны смотреть не на то, что происходит вокруг вызова, который создает волокно, а скорее на то, что происходит внутри волокна. Как только вы окажетесь внутри волокна, вы сможете программировать в стиле синхронизации. Например:

function f1 () {
    console.log ('подождите ...' + новая дата);
    сон (1000);
    console.log ('ок ...' + новая дата);   
}

function f2 () {
    f1 ();
    f1 ();
}

Волокно (function () {
    f2 ();
}).бегать();

Внутри волокна, которое вы вызываете f1, f2и sleepкак будто они синхронизированы.

В типичном веб-приложении вы создадите Fiber в диспетчере HTTP-запросов. Как только вы это сделаете, вы можете написать всю свою логику обработки запросов в стиле синхронизации, даже если она вызывает асинхронные функции (fs, базы данных и т. Д.).

Бруно Жуье
источник
Спасибо, Бруно. Но что, если мне нужен стиль синхронизации в коде начальной загрузки, который должен быть выполнен до того, как сервер привяжется к порту tcp - например, конфигурация или данные, которые должны быть прочитаны из базы данных, которая открывается асинхронно? Я мог закончить тем, что обернул весь server.js в Fiber, и я подозреваю, что это убьет параллелизм на уровне всего процесса. Тем не менее, это предложение стоит проверить. Для меня идеальное решение должно иметь возможность обернуть асинхронную функцию, чтобы обеспечить синтаксис вызова синхронизации и блокировать только следующие строки кода в цепочке вызывающего абонента, не жертвуя параллелизмом на уровне процесса.
abbr
Вы можете заключить весь свой код начальной загрузки в один большой вызов Fiber. Параллелизм не должен быть проблемой, потому что код начальной загрузки обычно должен выполняться до завершения, прежде чем вы начнете обслуживать запросы. Кроме того, волокно не препятствует запуску других волокон: каждый раз, когда вы нажимаете вызов yield, вы даете другим волокнам (и основному потоку) возможность работать.
Bruno Jouhier
Я обернул файл экспресс-загрузки server.js с помощью fiber. Последовательность выполнения - это то, что я ищу, но этот перенос никак не влияет на обработчик запросов. Поэтому я думаю, что нужно применить одну и ту же оболочку к КАЖДОМУ диспетчеру. Я сдался на этом этапе, потому что, похоже, не помогает избежать глобального рефакторинга. Моя цель - минимизировать влияние на вызывающего абонента, когда метод сбора данных изменяется с синхронного на асинхронный на уровне DAO, а Fiber все еще не справляется с задачей.
abbr
@fred: Нет смысла «синхронизировать» потоки событий, такие как обработчик запросов - вам понадобится while(true) handleNextRequest()цикл. Оборачивание каждого обработчика запросов в волокно будет.
Bergi
@fred: фибры не очень помогут вам с Express, потому что обратный вызов Express не является обратным вызовом продолжения (обратный вызов, который всегда вызывается ровно один раз, либо с ошибкой, либо с результатом). Но фибры решат пирамиду гибели, если у вас есть много кода, написанного поверх асинхронных API-интерфейсов с обратными вызовами продолжения (например, fs, mongodb и многие другие).
Bruno Jouhier
-2

Сначала я боролся с этим с помощью node.js, а async.js - лучшая библиотека, которую я нашел, чтобы помочь вам справиться с этим. Если вы хотите писать синхронный код с помощью узла, подход следующий.

var async = require('async');

console.log('in main');

doABunchOfThings(function() {
  console.log('back in main');
});

function doABunchOfThings(fnCallback) {
  async.series([
    function(callback) {
      console.log('step 1');
      callback();
    },
    function(callback) {
      setTimeout(callback, 1000);
    },
    function(callback) {
      console.log('step 2');
      callback();
    },
    function(callback) {
      setTimeout(callback, 2000);
    },
    function(callback) {
      console.log('step 3');
      callback();
    },
  ], function(err, results) {
    console.log('done with things');
    fnCallback();
  });
}

эта программа ВСЕГДА будет производить следующее ...

in main
step 1
step 2
step 3
done with things
back in main
Майкл Коннор
источник
2
asyncработает в вашем примере b / c main, который не заботится о вызывающем. Представьте, что весь ваш код заключен в функцию, которая должна возвращать результат одного из вызовов вашей асинхронной функции. Его можно легко проверить, добавив console.log('return');в конец кода. В этом случае вывод returnпроизойдет после, in mainно до step 1.
сокр
-11

Javascript - это однопоточный язык, вы не хотите блокировать весь свой сервер! Асинхронный код устраняет условия гонки, делая зависимости явными.

Научитесь любить асинхронный код!

Посмотрите на promisesасинхронный код, не создавая пирамиды ада обратных вызовов. Я рекомендую библиотеку обещаний для node.js

httpGet(url.parse("http://example.org/")).then(function (res) {
    console.log(res.statusCode);  // maybe 302
    return httpGet(url.parse(res.headers["location"]));
}).then(function (res) {
    console.log(res.statusCode);  // maybe 200
});

http://howtonode.org/promises

РЕДАКТИРОВАТЬ: это, безусловно, мой самый спорный ответ, теперь у узла есть ключевое слово yield, которое позволяет обрабатывать асинхронный код, как если бы он был синхронным. http://blog.alexmaccaw.com/how-yield-will-transform-node

roo2
источник
1
Promise только перефразирует параметр обратного вызова, а не превращает функцию в синхронизацию.
abbr
2
вы не хотите, чтобы это синхронизировалось, иначе весь ваш сервер заблокируется! stackoverflow.com/questions/17959663/…
roo2
1
Желательно, чтобы вызов синхронизации не блокировал другие события, такие как другой запрос, обрабатываемый Node.js. Функция Sync по определению означает только то, что она не вернется к вызывающему, пока не будет получен результат (а не просто обещание). Он не исключает сервер из обработки других событий, пока вызов заблокирован.
abbr
@fred: Я думаю, вы упускаете суть обещаний . Это не просто абстракция шаблона наблюдателя, но они предоставляют способ связывать и составлять асинхронные действия.
Берги
1
@Bergi, я много использую обещание и точно знаю, что он делает. Фактически все, что он достиг, - это разбиение одного вызова асинхронной функции на несколько вызовов / операторов. Но это не меняет результата - когда вызывающий объект возвращается, он не может вернуть результат асинхронной функции. Посмотрите пример, который я опубликовал в JSFiddle. В этом случае вызывающей стороной является функция AnticipatedSyncFunction, а функцией async - setTimeout. Если вы можете ответить на мой вызов с помощью обещания, пожалуйста, покажите мне.
abbr