Як перетворити виклики функції асинхронізації у функцію синхронізації в Node.js або Javascript?


122

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

Асинхронною функцією, що використовує зворотній зв'язок / обіцянку, є ДНК JavasSript і Node.js. Будь-який нетривіальний додаток JS, ймовірно, пронизаний цим стилем кодування. Але ця практика може легко призвести до так званої піраміди зворотного виклику приреченості. Ще гірше, якщо будь-який код будь-якого абонента в ланцюзі викликів залежить від результату функції асинхронізації, цей код також повинен бути включений у функцію зворотного виклику, накладаючи обмеження стилю кодування на абонента. Час від часу я знаходжу необхідність інкапсулювати функцію асинхронізації (часто надану у сторонній бібліотеці) у функцію синхронізації, щоб уникнути масового глобального пере-факторингу. Пошук рішення на цю тему зазвичай закінчувався за допомогою волокон Nodeабо npm-пакети, отримані з нього. Але Волокна просто не можуть вирішити проблему, з якою я стикаюся. Навіть приклад, поданий автором Волокна, ілюстрував недолік:

...
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
Для того, щоб дандавіс, асинхронізація додає деталі реалізації до ланцюжка викликів, іноді змушуючи глобальний рефакторинг. Це згубно і навіть згубно для складної програми, де важлива модуляризація та стримування.
абр

4
"Піраміда зворотного виклику приреченості" - це лише представлення проблеми. Обіцянка може приховати або замаскувати його, але не може вирішити справжнє завдання: Якщо виклик функції асинхронізації залежить від результатів функції асинхронізації, він повинен використовувати зворотний виклик, а також його виклик і т. Д. Це класичний приклад накладення обмежень на абонент просто через деталі реалізації.
абр

1
@abbr: Дякую за модуль deasync, опис вашої проблеми є саме тим, що я шукав, і не зміг знайти жодних ефективних рішень. Я поспілкувався з генераторами та ітерабелями, але прийшов до тих же висновків, що і ви.
Кевін Джангіані

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

Відповіді:


104

deasync перетворює функцію async в синхронізацію, реалізовану за допомогою механізму блокування, викликаючи цикл подій 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. Модуль був створений після публікації цього питання і не знайшов діючої пропозиції.)


Хтось ще мав удачу з цим? Я не можу змусити його працювати.
новачок

3
Я не можу змусити його працювати належним чином. вам слід покращити свою документацію для цього модуля, якщо ви хочете, щоб він більше використовувався. Сумніваюсь, автори точно знають, що таке наслідки для використання модуля, і якщо вони є, вони, безумовно, їх не документують.
Олександр Міллс

5
Поки є одна підтверджена проблема, задокументована в трекері випуску github. Проблема була виправлена ​​в Node v0.12. Решта, про які я знаю, - лише необґрунтовані домисли, які не варто документувати. Якщо ви вважаєте, що ваша проблема викликана дезасинхронізацією, опублікуйте автономний, копіюваний сценарій, і я розберуся.
абр

Я спробував використати його, і я отримав деякі вдосконалення в своєму сценарії, але все-таки мені не пощастило з датою. Я змінив код наступним чином: 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 секунди різних у вихідній даті!
Алекс

@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, якщо ваша мета - не обманювати людей.
Олександр Міллз

@AlexMills: Так, це справді було б жахливо . Однак, на щастя, це нічого, що може зробити API. Асинхронний API завжди повинен приймати зворотний виклик / повертати обіцянку / очікувати, що буде запущений всередині волокна - без нього не виходить. Афаїк, волокна в основному використовувались у швидких тридцяти сценаріях, які блокували та не мають жодної сукупності, але хочуть використовувати API async; так само, як і у вузлі, іноді трапляються випадки, коли ви використовуєте синхронні fsметоди.
Бергі

2
Я взагалі люблю вузол. Особливо, якщо я можу використовувати машинопис замість чистого js. Але вся ця нісенітниця асинхронізації, яка пронизує все, що ви робите, і буквально заражає кожну функцію в ланцюзі викликів, як тільки ви вирішите зробити один виклик асинхронізації - це те, що я насправді ... дуже ненавиджу. Async api - це як інфекційне захворювання, один виклик заражає всю вашу кодову базу, змушуючи вас переписати весь код, який у вас є. Я дійсно не розумію, як хтось може стверджувати, що це гарна річ.
Кріс

@Kris Node використовує асинхронну модель для завдань вводу-виводу, оскільки це швидко і просто. Ви також можете робити багато речей синхронно, але блокування відбувається повільно, оскільки ви нічого не можете робити одночасно - якщо тільки ви не шукаєте теми, які ускладнюють все.
Бергі

@Bergi Я читаю маніфест, щоб знати аргументи. Але змінити існуючий код на асинхронізацію, коли ви потрапили на перший виклик api, який не має еквівалента синхронізації, не просто. Все порушується, і кожен окремий рядок коду потрібно ретельно перевіряти. Якщо ваш код не є тривіальним, я гарантую ..., що конвертувати і змусити його знову працювати після перетворення всієї речі в ідіому асинхронізації знадобиться деякий час.
Кріс

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 не є асинхронізуванням, незважаючи на виклик функції асинхронізації.

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

EDIT: Я розумію, що багато разів вам потрібно ввімкнути функцію асинхронізації в функцію синхронізації, яка знаходиться всередині контролера. Для таких ситуацій, ось фокус партії:

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, де я визначив зворотній дзвінок, і випромініть подію, де я викликав зворотний виклик.


1

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

Розглянемо приклад: Скажімо, ви використовуєте деякий фреймворк, який є точкою входу вашої програми (ви не можете змінювати цю рамку). Цей фреймворк завантажує модулі nodejs як плагіни і викликає деякі методи в плагінах. Скажімо, цей фреймворк приймає лише синхронні функції, а сам не використовує волокна.

Є бібліотека, яку ви хочете використовувати в одному зі своїх плагінів, але ця бібліотека є асинхронною, і ви також не хочете її змінювати.

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

Нижня сторона: якщо рамка використовує setTimeoutабо Promises внутрішньо, то вона вийде з контексту волокна. Це можна обійти з допомогою глузливий setTimeout, Promise.thenі все обробники подій.

Таким чином, ви можете давати волокно, поки Promiseне вирішиться. Цей код виконує функцію асинхронізації (повернення обіцянки) і поновлює волокно, коли обіцянка буде вирішена:

Framework-entry.js

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

async-lib.js

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

my-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());
};

my-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 ().

Нормальний код Async. Це використовує функції CallBack.

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 (), wait () та 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

-1

Ви не повинні дивитися на те, що відбувається навколо дзвінка, який створює волокно, а на те, що відбувається всередині волокна. Після того, як ви перебуваєте всередині волокна, ви можете програмувати в стилі синхронізації. Наприклад:

функція f1 () {
    console.log ('зачекати ...' + нова дата);
    сон (1000);
    console.log ('ok ...' + нова дата);   
}

функція f2 () {
    f1 ();
    f1 ();
}

Волокна (функція () {
    f2 ();
}). run ();

Усередині волокна, яке ви називаєте f1, f2і sleepніби вони синхронізуються.

У звичайному веб-додатку ви створите Fiber у своєму диспетчері запитів HTTP. Після того, як ви зробите це, ви зможете записати всю логіку обробки запиту в стилі синхронізації, навіть якщо вона викликає функції асинхронізації (fs, бази даних тощо).


Спасибі Бруно. Але що робити, якщо мені потрібен стиль синхронізації в коді завантаження, який потрібно виконати перед тим, як сервер прив’язується до порту tcp - наприклад, конфігурація або дані, які потрібно читати з db, відкритого async? Я, можливо, перегорнув весь server.js у Fiber, і підозрюю, що це вб'є одночасність на всьому рівні процесу. Тим не менш, це пропозиція, яку варто перевірити. Для мене ідеальним рішенням повинно бути можливість включати функцію асинхронізації для надання синтаксису виклику синхронізації та блокує лише наступні рядки коду в ланцюзі абонента, не приносячи шкоди паралельності на рівні процесу.
абр

Ви можете загортати весь код завантаження всередині одного великого дзвінка Fiber. Паралельність не повинна бути проблемою, оскільки код завантаження зазвичай потрібно запустити до завершення, перш ніж ви почнете подавати запити. Також волокно не заважає бігти іншим волокнам: кожен раз, коли ви потрапляєте в дзвінок виходу, ви даєте іншим волокнам (і основній нитці) шанс запуститися.
Бруно Жуе

Я обгорнув експрес-файл завантажувального файлу server.js файлом. Я шукаю послідовність виконання, але ця обробка не впливає на обробник запиту. Тому я думаю, що потрібно застосувати ту саму обгортку до кожного диспетчера. Я відмовився в цей момент, тому що, здається, це не робить нічого кращого, щоб уникнути глобального переоцінки. Моя мета - мінімізувати вплив на абонента, коли метод збору даних змінюється з синхронізації на асинхронізацію в шарі DAO, а Fiber все ще трохи не відповідає проблемі.
абр

@fred: Не має особливого сенсу "синхронізувати" потоки подій, як обробник запитів - вам потрібно мати while(true) handleNextRequest()петлю. Обгортання кожного обробника запиту у волокно буде.
Бергі

@fred: волокна не допоможуть тобі з Express, оскільки зворотний виклик Express є не зворотний зворотний дзвінок (зворотний виклик, який завжди викликається точно один раз, або з помилкою, або з результатом). Але волокна вирішать піраміду приреченості, коли у вас буде багато коду, написаного поверх API асинхронізації із зворотними зворотними зворотами (наприклад, fs, mongodb та багато інших).
Бруно Жує

-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асинхронний код, не створюючи піраміди пекла зворотного дзвінка. Я рекомендую бібліотеку promisQ для 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

EDIT: це, безумовно, моя найсуперечливіша відповідь, у вузлі зараз є ключове слово "вихід", яке дозволяє розглядати код асинхронізації як би синхронним. http://blog.alexmaccaw.com/how-yield-will-transform-node


1
Обіцяйте лише перефразовувати параметр зворотного дзвінка, а не перетворювати функцію в синхронізацію.
абр

2
yuu не хочу, щоб синхронізувався або весь ваш сервер блокується! stackoverflow.com/questions/17959663 / ...
roo2

1
Що бажано - це виклик синхронізації без блокування інших подій, таких як інший запит, який обробляється Node.js. Функція синхронізації за визначенням означає лише, що вона не повернеться до абонента, поки результат не буде створений (не просто обіцянка). Це не попередньо виключає сервер від обробки інших подій, коли виклик заблокований.
абр

@fred: Я думаю , ти пропускаєш точку обіцянок . Вони не просто абстрагування структури спостерігачів, але вони забезпечують спосіб ланцюга та складання асинхронних дій.
Бергі

1
@Bergi, я багато обіцяю і точно знаю, що це робить. Ефективно все, що було досягнуто, - це розбиття однієї виклику функції асинхронізації на кілька викликів / операторів. Але це не змінює результат - коли абонент повертається, він не може повернути результат функції асинхронізації. Ознайомтеся з прикладом, який я розмістив у JSFiddle. Викликаючим в цьому випадку є функція AnticipatedSyncFunction, а функція асинхронізації - setTimeout. Якщо ви можете відповісти на мій виклик за допомогою обіцянки, будь ласка, покажіть мені.
абр
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.