Chamar funções assíncronas / aguardar em paralelo

433

Tanto quanto eu entendo, no ES7 / ES2016, colocar múltiplos await's em código funcionará de maneira semelhante ao encadeamento de .then()promessas, o que significa que eles serão executados um após o outro e não em paralelo. Então, por exemplo, temos este código:

await someCall();
await anotherCall();

Entendi corretamente que anotherCall()só será chamado quando someCall()for concluído? Qual é a maneira mais elegante de chamá-los em paralelo?

Eu quero usá-lo no Node, então talvez haja uma solução com a biblioteca assíncrona?

EDIT: Não estou satisfeito com a solução fornecida nesta pergunta: Desaceleração devido à espera paralela de promessas em geradores assíncronos , porque ele usa geradores e estou perguntando sobre um caso de uso mais geral.

Victor Marchuk
fonte
1
@adeneo Isso está incorreto, o Javascript nunca é executado em paralelo dentro de seu próprio contexto.
Blindman67
5
@ Blindman67 - funciona, pelo menos da maneira que o OP significa, onde duas operações assíncronas estão sendo executadas simultaneamente, mas não neste caso, o que eu queria escrever era que elas eram executadas em série , a primeira awaitaguardaria a conclusão da primeira função inteiramente antes de executar o segundo.
24416 adeneo
3
@ Blindman67 - é de thread único, mas essa limitação não se aplica aos métodos assíncronos, eles podem ser executados simultaneamente e retornar a resposta quando terminarem, ou seja, o que o OP quer dizer com "paralelo".
24416 adeneo
7
@ Blindman67 - Acho bem claro o que o OP está pedindo, o uso do padrão async / waitit fará com que as funções sejam executadas em série, mesmo que sejam assíncronas, para que o primeiro termine completamente antes do segundo ser chamado etc. O OP é perguntando como chamar ambas as funções em paralelo, e como elas são claramente assíncronas, o objetivo é executá-las simultaneamente, ou seja, em paralelo, por exemplo, fazendo duas solicitações ajax simultaneamente, o que não é um problema em javascript, como a maioria dos métodos assíncronos , como você observou, executa código nativo e usa mais threads.
22416 adeneo
3
@ Bergi, essa não é uma duplicata da questão vinculada - trata-se especificamente de sintaxe assíncrona / aguardada e Promises nativos . A questão vinculada é sobre a biblioteca bluebird com geradores e rendimento. Conceitualmente semelhante, talvez, mas não em implementação.
Iest

Respostas:

702

Você pode aguardar em Promise.all():

await Promise.all([someCall(), anotherCall()]);

Para armazenar os resultados:

let [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Observe que Promise.allfalha rapidamente, o que significa que, assim que uma das promessas fornecidas a ele for rejeitada, a coisa toda será rejeitada.

const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))

Promise.all([happy('happy', 100), sad('sad', 50)])
  .then(console.log).catch(console.log) // 'sad'

Se, em vez disso, você desejar aguardar todas as promessas cumpridas ou rejeitadas, poderá usá-lo Promise.allSettled. Observe que o Internet Explorer não oferece suporte nativamente a esse método.

const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))

Promise.allSettled([happy('happy', 100), sad('sad', 50)])
  .then(console.log) // [{ "status":"fulfilled", "value":"happy" }, { "status":"rejected", "reason":"sad" }]

madox2
fonte
78
Limpe, mas esteja ciente do comportamento de falha rápida do Promise.all. Se alguma das funções gerar um erro, Promise.all rejeitará
NoNameProvided 8/17/17
11
Você pode lidar bem com os resultados parciais com async /
waiting
131
Dica profissional: use a desestruturação da matriz para inicializar um número arbitrário de resultados de Promise.all (), como:[result1, result2] = Promise.all([async1(), async2()]);
jonny
10
@ Jonny Este assunto está sujeito a falha rápida? Além disso, ainda é necessário = await Promise.all?
theUtherSide
5
@theUtherSide Você está absolutamente certo - eu esqueci de incluir a espera.
jonny
114

TL; DR

Use Promise.allpara chamadas de função paralelas, o comportamento da resposta não está correto quando o erro ocorre.


Primeiro, execute todas as chamadas assíncronas de uma só vez e obtenha todos os Promiseobjetos. Segundo, use awaitnos Promiseobjetos. Dessa forma, enquanto você espera pela primeira Promisesolução, as outras chamadas assíncronas ainda estão em andamento. No geral, você só esperará enquanto a chamada assíncrona mais lenta. Por exemplo:

// Begin first call and store promise without waiting
const someResult = someCall();

// Begin second call and store promise without waiting
const anotherResult = anotherCall();

// Now we await for both results, whose async processes have already been started
const finalResult = [await someResult, await anotherResult];

// At this point all calls have been resolved
// Now when accessing someResult| anotherResult,
// you will have a value instead of a promise

Exemplo de JSbin: http://jsbin.com/xerifanima/edit?js,console

Advertência: não importa se as awaitchamadas estão na mesma linha ou em linhas diferentes, desde que a primeira awaitchamada ocorra após todas as chamadas assíncronas. Veja o comentário de JohnnyHK.


Atualização: esta resposta tem um tempo diferente no tratamento de erros, de acordo com a resposta do @ bergi , NÃO lança o erro à medida que o erro ocorre, mas depois que todas as promessas são executadas. Comparo o resultado com a dica de @ jonny:, [result1, result2] = Promise.all([async1(), async2()])verifique o seguinte snippet de código

const correctAsync500ms = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 500, 'correct500msResult');
  });
};

const correctAsync100ms = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 100, 'correct100msResult');
  });
};

const rejectAsync100ms = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'reject100msError');
  });
};

const asyncInArray = async (fun1, fun2) => {
  const label = 'test async functions in array';
  try {
    console.time(label);
    const p1 = fun1();
    const p2 = fun2();
    const result = [await p1, await p2];
    console.timeEnd(label);
  } catch (e) {
    console.error('error is', e);
    console.timeEnd(label);
  }
};

const asyncInPromiseAll = async (fun1, fun2) => {
  const label = 'test async functions with Promise.all';
  try {
    console.time(label);
    let [value1, value2] = await Promise.all([fun1(), fun2()]);
    console.timeEnd(label);
  } catch (e) {
    console.error('error is', e);
    console.timeEnd(label);
  }
};

(async () => {
  console.group('async functions without error');
  console.log('async functions without error: start')
  await asyncInArray(correctAsync500ms, correctAsync100ms);
  await asyncInPromiseAll(correctAsync500ms, correctAsync100ms);
  console.groupEnd();

  console.group('async functions with error');
  console.log('async functions with error: start')
  await asyncInArray(correctAsync500ms, rejectAsync100ms);
  await asyncInPromiseAll(correctAsync500ms, rejectAsync100ms);
  console.groupEnd();
})();

Refúgio
fonte
11
Parece-me uma opção muito mais agradável do que Promise.all - e com a atribuição de desestruturação, você pode fazer [someResult, anotherResult] = [await someResult, await anotherResult]se mudar constpara let.
Jawj 25/08
28
Mas isso ainda executa as awaitinstruções em série, certo? Ou seja, a execução pausa até que o primeiro seja awaitresolvido e depois passa para o segundo. Promise.allexecuta em paralelo.
Andru
8
Obrigado @Haven. Essa deve ser a resposta aceita.
Stefan D
87
Essa resposta é enganosa, pois o fato de que ambas as esperas são feitas na mesma linha é irrelevante. O que importa é que as duas chamadas assíncronas sejam feitas antes que uma das duas seja aguardada.
JohnnyHK
15
@Haven esta solução não é a mesma que Promise.all. Se cada solicitação for uma chamada de rede, await someResultprecisará ser resolvida antes await anotherResultmesmo de começar. Por outro lado, nas Promise.allduas awaitchamadas podem ser iniciadas antes de qualquer uma ser resolvida.
Ben Winding
89

Atualizar:

A resposta original torna difícil (e em alguns casos impossível) lidar corretamente com as rejeições de promessas. A solução correta é usar Promise.all:

const [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Resposta original:

Apenas certifique-se de chamar as duas funções antes de aguardar uma:

// Call both functions
const somePromise = someCall();
const anotherPromise = anotherCall();

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
fonte
1
@JeffFischer Adicionei comentários que esperamos torná-lo mais claro.
Jonathan Potter
9
Eu sinto que esta é certamente a resposta mais pura
Gershom
1
Essa resposta é muito mais clara que a de Haven. É claro que as chamadas de função retornarão objetos de promessa e awaitos resolverão em valores reais.
user1032613
3
Isso parece funcionar rapidamente, mas tem problemas horríveis com rejeições não tratadas . Não use isso!
Bergi 20/01/19
1
@ Bergi Você está certo, obrigado por apontar isso! Atualizei a resposta com uma solução melhor.
Jonathan Potter
24

Existe outra maneira, sem Promise.all (), de fazer isso em paralelo:

Primeiro, temos 2 funções para imprimir números:

function printNumber1() {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
      console.log("Number1 is done");
      resolve(10);
      },1000);
   });
}

function printNumber2() {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
      console.log("Number2 is done");
      resolve(20);
      },500);
   });
}

Isso é seqüencial:

async function oneByOne() {
   const number1 = await printNumber1();
   const number2 = await printNumber2();
} 
//Output: Number1 is done, Number2 is done

Isto é paralelo:

async function inParallel() {
   const promise1 = printNumber1();
   const promise2 = printNumber2();
   const number1 = await promise1;
   const number2 = await promise2;
}
//Output: Number2 is done, Number1 is done
user2883596
fonte
10

Isso pode ser feito com Promise.allSettled () , que é semelhante, Promise.all()mas sem o comportamento à prova de falhas.

async function failure() {
    throw "Failure!";
}

async function success() {
    return "Success!";
}

const [failureResult, successResult] = await Promise.allSettled([failure(), success()]);

console.log(failureResult); // {status: "rejected", reason: "Failure!"}
console.log(successResult); // {status: "fulfilled", value: "Success!"}

Nota : Esta é uma característica borda do sangramento com suporte ao navegador limitado, então eu fortemente recomendo incluindo um polyfill para esta função.

Jonathan Sudiaman
fonte
7

Eu criei uma essência testando maneiras diferentes de resolver promessas, com resultados. Pode ser útil ver as opções que funcionam.

SkarXa
fonte
Os testes 4 e 6 na essência retornaram os resultados esperados. Consulte stackoverflow.com/a/42158854/5683904 por NoNameProvided, que explica a diferença entre as opções.
akraines
1
    // A generic test function that can be configured 
    // with an arbitrary delay and to either resolve or reject
    const test = (delay, resolveSuccessfully) => new Promise((resolve, reject) => setTimeout(() => {
        console.log(`Done ${ delay }`);
        resolveSuccessfully ? resolve(`Resolved ${ delay }`) : reject(`Reject ${ delay }`)
    }, delay));

    // Our async handler function
    const handler = async () => {
        // Promise 1 runs first, but resolves last
        const p1 = test(10000, true);
        // Promise 2 run second, and also resolves
        const p2 = test(5000, true);
        // Promise 3 runs last, but completes first (with a rejection) 
        // Note the catch to trap the error immediately
        const p3 = test(1000, false).catch(e => console.log(e));
        // Await all in parallel
        const r = await Promise.all([p1, p2, p3]);
        // Display the results
        console.log(r);
    };

    // Run the handler
    handler();
    /*
    Done 1000
    Reject 1000
    Done 5000
    Done 10000
    */

Embora a configuração de p1, p2 e p3 não os execute estritamente em paralelo, eles não suportam nenhuma execução e você pode capturar erros contextuais com uma captura.

Thrunobulax
fonte
2
Bem-vindo ao Stack Overflow. Embora seu código possa fornecer a resposta para a pergunta, adicione um contexto ao redor para que outras pessoas tenham alguma idéia do que fazem e por que estão lá.
Theo
1

No meu caso, tenho várias tarefas que quero executar em paralelo, mas preciso fazer algo diferente com o resultado dessas tarefas.

function wait(ms, data) {
    console.log('Starting task:', data, ms);
    return new Promise(resolve => setTimeout(resolve, ms, data));
}

var tasks = [
    async () => {
        var result = await wait(1000, 'moose');
        // do something with result
        console.log(result);
    },
    async () => {
        var result = await wait(500, 'taco');
        // do something with result
        console.log(result);
    },
    async () => {
        var result = await wait(5000, 'burp');
        // do something with result
        console.log(result);
    }
]

await Promise.all(tasks.map(p => p()));
console.log('done');

E a saída:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
fonte
legal para criação dinâmica (matriz de recursos)
Michal Miky Jankovský
1

aguarde Promise.all ([someCall (), anotherCall ()]); como já mencionado, ele funcionará como uma cerca de encadeamento (muito comum em código paralelo como CUDA), portanto, permitirá que todas as promessas sejam executadas sem bloquear uma à outra, mas impedirá a execução de continuar até que TODAS sejam resolvidas.

outra abordagem que vale a pena compartilhar é o Node.js. assíncrono que também permitirá que você controle facilmente a quantidade de simultaneidade que normalmente é desejável se a tarefa estiver diretamente vinculada ao uso de recursos limitados como chamada de API, operações de E / S, etc.

// create a queue object with concurrency 2
var q = async.queue(function(task, callback) {
  console.log('Hello ' + task.name);
  callback();
}, 2);

// assign a callback
q.drain = function() {
  console.log('All items have been processed');
};

// add some items to the queue
q.push({name: 'foo'}, function(err) {
  console.log('Finished processing foo');
});

q.push({name: 'bar'}, function (err) {
  console.log('Finished processing bar');
});

// add some items to the queue (batch-wise)
q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
  console.log('Finished processing item');
});

// add some items to the front of the queue
q.unshift({name: 'bar'}, function (err) {
  console.log('Finished processing bar');
});

Créditos ao autor do artigo Medium ( leia mais )

Thiago Conrado
fonte
-5

Eu voto em:

await Promise.all([someCall(), anotherCall()]);

Esteja ciente do momento em que você chama funções, isso pode causar resultados inesperados:

// Supposing anotherCall() will trigger a request to create a new User

if (callFirst) {
  await someCall();
} else {
  await Promise.all([someCall(), anotherCall()]); // --> create new User here
}

Mas seguir sempre aciona a solicitação para criar um novo usuário

// Supposing anotherCall() will trigger a request to create a new User

const someResult = someCall();
const anotherResult = anotherCall(); // ->> This always creates new User

if (callFirst) {
  await someCall();
} else {
  const finalResult = [await someResult, await anotherResult]
}
Hoang Le Anh Tu
fonte
Desde que você declara a função fora / antes do teste de condição e as chamou. Tente envolvê-los em elsebloco.
Haven
@ Haven: quero dizer, quando você separa os momentos em que você chama funções vs espera pode levar a resultados inesperados, por exemplo: solicitações HTTP assíncronas.
Hoang Le Anh Tu
-6

Eu crio uma função auxiliar waitAll, pode ser que possa torná-la mais doce. Por enquanto, ele só funciona no nodejs , não no chrome do navegador.

    //const parallel = async (...items) => {
    const waitAll = async (...items) => {
        //this function does start execution the functions
        //the execution has been started before running this code here
        //instead it collects of the result of execution of the functions

        const temp = [];
        for (const item of items) {
            //this is not
            //temp.push(await item())
            //it does wait for the result in series (not in parallel), but
            //it doesn't affect the parallel execution of those functions
            //because they haven started earlier
            temp.push(await item);
        }
        return temp;
    };

    //the async functions are executed in parallel before passed
    //in the waitAll function

    //const finalResult = await waitAll(someResult(), anotherResult());
    //const finalResult = await parallel(someResult(), anotherResult());
    //or
    const [result1, result2] = await waitAll(someResult(), anotherResult());
    //const [result1, result2] = await parallel(someResult(), anotherResult());
Fred Yang
fonte
3
Não, a paralelização não está acontecendo aqui. O forloop espera sequencialmente cada promessa e adiciona o resultado à matriz.
Szczepan Hołyszewski
Entendo que isso parece não funcionar para as pessoas. Então eu testei no node.js e no navegador. O teste é passado no node.js (v10, v11), firefox, ele não funciona no navegador chrome. O caso de teste está em gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang
2
Eu me recuso a acreditar nisso. Não há nada no padrão que diga que diferentes iterações de um loop for possam ser paralelizadas automaticamente; não é assim que o javascript funciona. A maneira como o código do loop é gravado significa : "aguarde um item (a espera expr), ENTÃO pressione resultado para temp, ENTÃO pegue o próximo item (próxima iteração do loop for)." Aguardando "para cada item confinado a uma única iteração do loop Se os testes mostram que há paralelização, deve ser porque o transpiler está fazendo algo fora do padrão ou está fora plana buggy..
Szczepan Hołyszewski
@ SzczepanHołyszewski Sua confiança em desacreditar sem executar o caso de teste me inspira a renomear comentários refinados e extras. Todo o código é simples ES6 antigo, não é necessária a transpilação.
Fred Yang
Não sei por que isso é tão votado com tanta força. É essencialmente a mesma resposta que o @ user2883596 deu.
Jonathan Sudiaman 6/04