Como agrupar chamadas de funções assíncronas em uma função de sincronização no Node.js. ou Javascript?

122

Suponha que você mantenha uma biblioteca que expõe uma função getData. Seus usuários chamá-lo para obter dados reais:
var output = getData();
Sob a dados capô é salvo em um arquivo para que você implementado getDatausando Node.js built-in fs.readFileSync. É óbvio ambos getDatae fs.readFileSyncsão funções de sincronização. Um dia, foi solicitado que você alternasse a fonte de dados subjacente para um repositório como o MongoDB, que só pode ser acessado de forma assíncrona. Você também foi instruído a evitar irritar seus usuários. A getDataAPI não pode ser alterada para retornar apenas uma promessa ou exigir um parâmetro de retorno de chamada. Como você atende a ambos os requisitos?

Função assíncrona usando retorno de chamada / promessa é o DNA de JavasSript e Node.js. Qualquer aplicativo JS não trivial provavelmente é permeado por esse estilo de codificação. Mas essa prática pode facilmente levar à chamada pirâmide de destruição de retorno de chamada. Pior ainda, se qualquer código em qualquer chamador na cadeia de chamadas depende do resultado da função assíncrona, esse código também deve ser envolvido na função de retorno de chamada, impondo uma restrição de estilo de codificação ao chamador. De tempos em tempos, acho necessário encapsular uma função assíncrona (geralmente fornecida em uma biblioteca de terceiros) em uma função de sincronização para evitar uma re-fatoração global maciça. Procurar uma solução sobre esse assunto geralmente terminava com fibras de nóou pacotes npm derivados dele. Mas as fibras simplesmente não conseguem resolver o problema que estou enfrentando. Até o exemplo fornecido pelo autor da Fibers ilustrou a deficiência:

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

Saída real:

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)

Se a função Fiber realmente transformar a função assíncrona em suspensão em sincronização, a saída deverá ser:

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

Eu criei outro exemplo simples no JSFiddle e procurando por código para produzir a saída esperada. Aceitarei uma solução que só funcione no Node.js, para que você possa solicitar qualquer pacote npm, apesar de não trabalhar no JSFiddle.

abbr
fonte
2
As funções assíncronas nunca podem ser sincronizadas no Node e, mesmo que pudessem, você não deveria. O problema é tal que, no módulo fs, você pode ver funções completamente separadas para acesso síncrono e assíncrono ao sistema de arquivos. O melhor que você pode fazer é mascarar a aparência do assíncrono com promessas ou corotinas (geradores no ES6). Para gerenciar pirâmides de retorno de chamada, forneça nomes a eles em vez de definir em uma chamada de função e use algo como a biblioteca assíncrona.
qubyte
8
Para os dandavis, o assíncrono elabora detalhes da implementação na cadeia de chamadas, às vezes forçando a refatoração global. Isso é prejudicial e até desastroso para uma aplicação complexa em que a modularização e a contenção são importantes.
abreviatura
4
"Pirâmide de retorno de desgraça" é apenas a representação do problema. A promessa pode ocultar ou disfarçar, mas não pode enfrentar o verdadeiro desafio: se o chamador de uma função assíncrona depende dos resultados da função assíncrona, deve usar retorno de chamada, assim como o chamador, etc. Esse é um exemplo clássico de imposição de restrições a chamador simplesmente por causa dos detalhes da implementação.
abreviatura
1
@abbr: Obrigado pelo módulo deasync, a descrição do seu problema é exatamente o que eu estava procurando e não foi possível encontrar nenhuma solução viável. Eu brinquei com geradores e iterables, mas cheguei às mesmas conclusões que você.
Kevin Jhangiani
2
Vale a pena notar que quase nunca é uma boa idéia forçar uma função assíncrona a ser sincronizada. Você quase sempre tem uma solução melhor que mantém intacta a assíncrona a função, enquanto ainda obtém o mesmo efeito (como seqüenciamento, configuração de variáveis, etc.).
Ghost de Madara

Respostas:

104

O deasync transforma a função assíncrona em sincronização, implementada com um mecanismo de bloqueio, chamando o loop de eventos Node.js. na camada JavaScript. Como resultado, o deasync apenas bloqueia a execução do código subseqüente sem bloquear o encadeamento inteiro, nem gera a espera ocupada. Com este módulo, aqui está a resposta para o desafio 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)

(isenção de responsabilidade: eu sou o co-autor de deasync. O módulo foi criado após a postagem desta pergunta e não encontrou nenhuma proposta viável.)

abbr
fonte
Alguém mais teve sorte com isso? Eu não posso fazer isso funcionar.
Newman
3
Não consigo fazer o trabalho corretamente. você deve melhorar sua documentação para este módulo, se desejar que ele seja usado mais. Duvido que os autores saibam exatamente quais são as ramificações para usar o módulo e, se o fizerem, certamente não os documentarão.
Alexander Mills
5
Até agora, há um problema confirmado documentado no rastreador de problemas do github. O problema foi corrigido no nó v0.12. O resto que conheço são apenas especulações infundadas que não valem a pena documentar. Se você acredita que seu problema é causado pelo deasync, poste um cenário duplicável e independente, e examinarei.
abbr
Tentei usá-lo e obtive algumas melhorias no meu script, mas ainda não tive sorte com a data. Modifiquei o código da seguinte maneira: 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); e espero ver 3 segundos de diferença na saída da data!
Alex
@abbr isso pode ser browserified e usados sem nó dependency>
Gandhi
5

Também existe um módulo de sincronização npm. que é usado para sincronizar o processo de execução da consulta.

Quando você deseja executar consultas paralelas de maneira síncrona, o nó é restrito para fazer isso porque nunca espera pela resposta. e o módulo de sincronização são muito perfeitos para esse tipo de solução.

Código de amostra

/*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);
                }
            });
        });
    }

link de referência: https://www.npmjs.com/package/sync

sanjeev kumar
fonte
4

Se a função Fiber realmente transformar a função assíncrona em suspensão em sincronização

Sim. Dentro da fibra, a função aguarda antes do log ok. As fibras não tornam as funções assíncronas síncronas, mas permitem escrever código com aparência síncrona que usa funções assíncronas e, em seguida, serão executadas de forma assíncrona dentro de a Fiber.

De tempos em tempos, acho necessário encapsular uma função assíncrona em uma função de sincronização, a fim de evitar uma re-fatoração global maciça.

Você não pode. É impossível tornar o código assíncrono síncrono. Você precisará antecipar isso em seu código global e escrevê-lo no estilo assíncrono desde o início. Se você agrupa o código global em uma fibra, usa promessas, geradores de promessas ou retornos de chamada simples, depende de suas preferências.

Meu objetivo é minimizar o impacto no chamador quando o método de aquisição de dados é alterado de sincronizado para assíncrono

Promessas e fibras podem fazer isso.

Bergi
fonte
1
esta é a pior coisa ABSOLUTA que você pode com o Node.js: "código de aparência síncrona que usa funções assíncronas e, em seguida, será executado de forma assíncrona." se sua API fizer isso, você arruinará vidas. se for assíncrono, deve exigir um retorno de chamada e gerar um erro se nenhum retorno de chamada for fornecido. essa é a melhor maneira de criar uma API, a menos que seu objetivo seja enganar as pessoas.
Alexander Mills
@ AlexMills: Sim, isso seria realmente horrível . No entanto, felizmente, isso não é nada que uma API possa fazer. Uma API assíncrona sempre precisa aceitar um retorno de chamada / retornar uma promessa / esperar que seja executada dentro de uma fibra - ela não funciona sem ela. Afaik, as fibras foram usadas principalmente em scripts quick'n'dirty que estavam bloqueando e não têm simultaneidade, mas desejam usar APIs assíncronas; assim como no nó, às vezes há casos em que você usaria os fsmétodos síncronos .
Bergi 02/06/2015
2
Eu geralmente gosto de nó. Especialmente se eu puder usar texto datilografado em vez de js puro. Mas toda essa bobagem assíncrona que permeia tudo o que você faz e literalmente infecta todas as funções da cadeia de chamadas assim que você decide fazer uma única chamada assíncrona é algo que eu realmente ... realmente odeio. A API assíncrona é como uma doença infecciosa, uma chamada infecta toda a sua base de códigos, forçando-o a reescrever todo o código que você possui. Realmente não entendo como alguém pode argumentar que isso é uma coisa boa .
Kris
O @Kris Node usa um modelo assíncrono para tarefas de E / S porque é rápido e simples. Você também pode fazer muitas coisas de forma síncrona, mas o bloqueio é lento, pois você não pode fazer nada ao mesmo tempo - a menos que opte por threads, o que torna tudo complicado.
Bergi 29/05
@ Bergi Eu li o manifesto para conhecer os argumentos. Mas alterar o código existente para assíncrono no momento em que você pressiona a primeira chamada da API que não tem equivalente de sincronização não é simples. Tudo quebra e cada linha de código precisa ser examinada. A menos que seu código seja trivial, eu garanto ... levará um tempo para convertê-lo e fazê-lo funcionar novamente depois de converter tudo para o idioma assíncrono.
Kris
2

Você precisa usar promessas:

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

Eu gosto mais de definições de funções de seta. Mas qualquer string do formato "() => {...}" também pode ser escrita como "function () {...}"

Portanto, o topDog não é assíncrono, apesar de chamar uma função assíncrona.

insira a descrição da imagem aqui

Edição: Eu percebo muitas vezes que você precisa para quebrar uma função assíncrona dentro de uma função de sincronização está dentro de um controlador. Para essas situações, aqui está um truque de festa:

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.
}

Utilizando isso com retornos de chamada, você pode executar um wrap que não use promessas:

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

Ao aplicar esse truque a um EventEmitter, você pode obter os mesmos resultados. Defina o ouvinte do EventEmitter onde defini o retorno de chamada e emita o evento em que chamei o retorno de chamada.

user2485309
fonte
1

Não consigo encontrar um cenário que não possa ser resolvido usando fibras de nó. O exemplo que você forneceu usando fibras de nó se comporta conforme o esperado. A chave é executar todo o código relevante dentro de uma fibra, para que você não precise iniciar uma nova fibra em posições aleatórias.

Vamos ver um exemplo: Digamos que você use alguma estrutura, que é o ponto de entrada do seu aplicativo (você não pode modificar essa estrutura). Essa estrutura carrega os módulos nodejs como plug-ins e chama alguns métodos nos plug-ins. Digamos que essa estrutura aceite apenas funções síncronas e não use fibras por si só.

Há uma biblioteca que você deseja usar em um de seus plug-ins, mas essa biblioteca é assíncrona e você também não deseja modificá-la.

O encadeamento principal não pode ser produzido quando nenhuma fibra está em execução, mas você ainda pode criar plugins usando fibras! Basta criar uma entrada de wrapper que inicie toda a estrutura dentro de uma fibra, para que você possa executar a execução a partir dos plug-ins.

Desvantagem: se a estrutura usa setTimeoutou Promises internamente, ela escapará do contexto da fibra. Isso pode ser contornado zombando setTimeout, Promise.thene todos os manipuladores de eventos.

Portanto, é assim que você pode produzir uma fibra até que uma Promiseseja resolvida. Este código utiliza uma função assíncrona (retorno da promessa) e retoma a fibra quando a promessa é resolvida:

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

Quando você executa node framework-entry.jsele irá lançar um erro: Error: yield() called with no fiber running. Se você executar, node my-entry.jsele funcionará conforme o esperado.

Tamas Hegedus
fonte
0

A sincronização de código do Node.js. é essencial em alguns aspectos, como banco de dados. Mas a vantagem real do Node.js está no código assíncrono. Como é um thread único sem bloqueio.

podemos sincronizá-lo usando funcionalidades importantes Fiber () Use waitit () e defer () que chamamos de todos os métodos usando waitit (). substitua as funções de retorno de chamada por defer ().

Código assíncrono normal. Isso usa funções de retorno de chamada.

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

Sincronize o código acima usando Fiber (), waitit () e adiado ()

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

});

Espero que isso ajude. Obrigado

Mohan Ramakrishna
fonte
0

Atualmente, esse padrão de gerador pode ser uma solução em muitas situações.

Aqui um exemplo de console seqüencial solicita no nodejs usando a função assíncrona 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
fonte
-1

Você não deve olhar para o que acontece em torno da chamada que cria a fibra, mas para o que acontece dentro da fibra. Uma vez dentro da fibra, você pode programar no estilo de sincronização. Por exemplo:

função f1 () {
    console.log ('aguarde ...' + nova data);
    dormir (1000);
    console.log ('ok ...' + nova data);   
}

função f2 () {
    f1 ();
    f1 ();
}

Fibra (função () {
    f2 ();
}).corre();

No interior da fibra que você chama f1, f2e sleepcomo se fossem sincronizados.

Em um aplicativo Web típico, você criará o Fiber no seu despachante de solicitações HTTP. Depois de fazer isso, você poderá gravar toda a lógica de manipulação de solicitações no estilo de sincronização, mesmo que ela chame funções assíncronas (fs, bancos de dados etc.).

Bruno Jouhier
fonte
Obrigado Bruno. Mas e se eu precisar de um estilo de sincronização no código de inicialização que precisa ser executado antes que o servidor seja vinculado à porta tcp - como configurações ou dados que precisam ser lidos do db que é aberto de forma assíncrona? Posso acabar envolvendo o server.js inteiro no Fiber, e suspeito que isso acabará com a concorrência em todo o nível do processo. No entanto, é uma sugestão que vale a pena verificar. Para mim, a solução ideal deve ser capaz de agrupar uma função assíncrona para fornecer uma sintaxe de chamada de sincronização e bloquear apenas as próximas linhas de código na cadeia de chamadas, sem sacrificar a simultaneidade no nível do processo.
abreviatura
Você pode agrupar todo o seu código de inicialização em uma grande chamada de fibra. A simultaneidade não deve ser um problema, porque o código de auto-inicialização geralmente precisa ser executado antes de você começar a atender solicitações. Além disso, uma fibra não impede que outras fibras funcionem: toda vez que você pressiona uma chamada de rendimento, você oferece a outras fibras (e à linha principal) uma chance de executar.
Bruno Jouhier 17/02
Enrolei o arquivo de inicialização Express server.js com fibra. A sequência de execução é o que estou procurando, mas esse empacotamento não tem nenhum efeito no manipulador de solicitações. Então eu acho que tenho que aplicar o mesmo wrapper para CADA expedidor. Desisti neste momento porque parece que não é melhor para ajudar a evitar a re-factoring global. Meu objetivo é minimizar o impacto no chamador quando o método de aquisição de dados é alterado de sincronizado para assíncrono na camada DAO e o Fiber ainda fica um pouco aquém do desafio.
abreviatura
@fred: Não faz muito sentido "sincronizar" os fluxos de eventos como o manipulador de solicitações - você precisaria ter um while(true) handleNextRequest()loop. Encapsular cada manipulador de solicitação em uma fibra faria isso.
Bergi
@fred: fibres não ajudará muito com o Express, porque o retorno de chamada do Express não é um retorno de chamada de continuação (um retorno de chamada que sempre é chamado exatamente uma vez, seja com erro ou com resultado). Mas as fibras resolverão a pirâmide da destruição quando você tiver muito código escrito nas APIs assíncronas com retornos de chamada de continuação (como fs, mongodb e muitos outros).
Bruno Jouhier
-2

Lutei com isso no começo com o node.js e o async.js é a melhor biblioteca que encontrei para ajudá-lo a lidar com isso. Se você deseja escrever código síncrono com o nó, a abordagem é assim.

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

este programa SEMPRE produzirá o seguinte ...

in main
step 1
step 2
step 3
done with things
back in main
Michael Connor
fonte
2
asyncfunciona no seu exemplo porque é main, o que não se importa com o chamador. Imagine que todo o seu código está envolvido em uma função que deve retornar o resultado de uma de suas chamadas de função assíncronas. Pode ser facilmente comprovado que não funciona, adicionando console.log('return');no final do seu código. Nesse caso, a saída de returnacontecerá depois, in mainmas antes step 1.
abbr
-11

Javascript é uma linguagem única, você não deseja bloquear todo o servidor! O código assíncrono elimina as condições de corrida, explicitando as dependências.

Aprenda a amar código assíncrono!

Dê uma olhada no promisescódigo assíncrono sem criar uma pirâmide do inferno de retorno de chamada. Eu recomendo a biblioteca promessaQ para 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: esta é de longe a minha resposta mais controversa, o nó agora possui yield keyword, que permite tratar o código assíncrono como se fosse síncrono. http://blog.alexmaccaw.com/how-yield-will-transform-node

roo2
fonte
1
Promise apenas reformula um parâmetro de retorno de chamada em vez de transformar a função em sincronização.
abreviatura
2
você não quer que seja sincronizado ou todo o servidor bloqueará! stackoverflow.com/questions/17959663/…
roo2
1
O que é desejável é uma chamada de sincronização sem bloquear outros eventos, como outra solicitação sendo tratada pelo Node.js. Uma função de sincronização por definição significa apenas que ela não retornará ao chamador até que o resultado seja produzido (não apenas uma promessa). Isso não impede o servidor de manipular outros eventos enquanto a chamada está bloqueada.
abreviatura
@ Fred: Eu acho que você está perdendo o ponto de promessas . Eles não são simplesmente uma abstração de padrão de observador, mas fornecem uma maneira de encadear e compor ações assíncronas.
Bergi 17/02
1
@ Bergi, eu uso muito a promessa e sei exatamente o que ela faz. Efetivamente, tudo o que foi alcançado é dividir uma única chamada de função assíncrona em várias invocações / instruções. Mas isso não altera o resultado - quando o chamador retorna, não pode retornar o resultado da função assíncrona. Confira o exemplo que eu publiquei no JSFiddle. O chamador nesse caso é a função AnticipatedSyncFunction e a função assíncrona é setTimeout. Se você pode responder ao meu desafio usando a promessa, me mostre.
abreviatura