Panggil fungsi asink / tunggu secara paralel

434

Sejauh yang saya mengerti, dalam ES7 / ES2016 menempatkan multiple awaitdalam kode akan bekerja sama dengan chaining .then()dengan janji, yang berarti bahwa mereka akan mengeksekusi satu demi satu daripada di parallerl. Jadi, misalnya, kami memiliki kode ini:

await someCall();
await anotherCall();

Apakah saya memahaminya dengan benar yang anotherCall()akan dipanggil hanya ketika someCall()selesai? Apa cara paling elegan untuk memanggil mereka secara paralel?

Saya ingin menggunakannya di Node, jadi mungkin ada solusi dengan pustaka async?

EDIT: Saya tidak puas dengan solusi yang disediakan dalam pertanyaan ini: Perlambatan karena janji yang tidak paralel menunggu di generator async , karena menggunakan generator dan saya bertanya tentang kasus penggunaan yang lebih umum.

Victor Marchuk
sumber
1
@ adeneo Itu salah, Javascript tidak pernah berjalan secara paralel dalam konteksnya sendiri.
Blindman67
5
@ Blindman67 - tidak, setidaknya seperti OP artinya, di mana dua operasi async berjalan secara bersamaan, tetapi tidak dalam kasus ini, apa yang saya maksudkan adalah bahwa mereka berjalan secara serial , yang pertama awaitakan menunggu fungsi pertama untuk menyelesaikan seluruhnya sebelum menjalankan yang kedua.
adeneo
3
@ Blindman67 - ini single threaded, tetapi batasan itu tidak berlaku untuk metode async, mereka dapat berjalan secara bersamaan, dan mengembalikan respons ketika mereka selesai, yaitu apa yang OP maksud dengan "parallell".
adeneo
7
@ Blindman67 - Saya pikir cukup jelas apa yang diminta OP, menggunakan pola async / wait akan membuat fungsi berjalan secara serial, bahkan jika async, jadi yang pertama akan selesai sebelum yang kedua dipanggil dll. OP adalah menanyakan cara memanggil kedua fungsi dalam parallell, dan karena keduanya jelas async, tujuannya adalah untuk menjalankannya secara bersamaan, yaitu dalam parallell, misalnya melakukan dua permintaan ajax secara bersamaan, yang sama sekali bukan masalah di javascript, karena kebanyakan metode async , seperti yang telah Anda catat, menjalankan kode asli, dan menggunakan lebih banyak utas.
adeneo
3
@Bergi ini bukan duplikat dari pertanyaan terkait - ini secara khusus tentang async / menunggu sintaks dan asli Promise. Pertanyaan terkait adalah tentang perpustakaan burung biru dengan generator & hasil. Secara konseptual serupa mungkin, tetapi tidak dalam implementasi.
Iest

Jawaban:

703

Anda bisa menunggu di Promise.all():

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

Untuk menyimpan hasilnya:

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

Perhatikan bahwa Promise.allgagal dengan cepat, yang berarti bahwa begitu salah satu janji yang diberikan kepadanya ditolak, maka semuanya ditolak.

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'

Sebaliknya, jika Anda ingin menunggu semua janji untuk dipenuhi atau ditolak, maka Anda dapat menggunakannya Promise.allSettled. Perhatikan bahwa Internet Explorer tidak mendukung metode ini.

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
sumber
79
Bersihkan tapi waspadai perilaku Promise.all yang gagal cepat. Jika salah satu fungsi melempar kesalahan, Promise.all akan menolak
NoNameProvided
11
Anda dapat menangani hasil sebagian dengan baik dengan async / menunggu, lihat stackoverflow.com/a/42158854/2019689
NoNameProvided
131
Kiat pro: gunakan susunan array untuk menginisialisasi sejumlah hasil arbitrer dari Promise.all (), seperti:[result1, result2] = Promise.all([async1(), async2()]);
jonny
10
@jonny Apakah ini bisa gagal-cepat? Juga, apakah masih perlu = await Promise.all?
theUtherSide
5
@theUtherSide Anda benar sekali - saya lupa menyertakan penantian.
jonny
114

TL; DR

Gunakan Promise.alluntuk panggilan fungsi paralel, perilaku jawaban tidak benar ketika kesalahan terjadi.


Pertama, jalankan semua panggilan asinkron sekaligus dan dapatkan semua Promiseobjek. Kedua, gunakan awaitpada Promiseobjek. Dengan cara ini, selagi Anda menunggu yang pertama Promiseuntuk menyelesaikan panggilan asinkron lainnya masih berlangsung. Secara keseluruhan, Anda hanya akan menunggu selama panggilan asinkron paling lambat. Sebagai contoh:

// 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

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

Peringatan: Tidak masalah jika awaitpanggilan berada di jalur yang sama atau di jalur yang berbeda, selama awaitpanggilan pertama terjadi setelah semua panggilan tidak sinkron. Lihat komentar JohnnyHK.


Pembaruan: jawaban ini memiliki waktu yang berbeda dalam penanganan kesalahan sesuai dengan jawaban @bergi , ia TIDAK membuang kesalahan saat kesalahan terjadi tetapi setelah semua janji dijalankan. Saya membandingkan hasilnya dengan tip @ jonny:, [result1, result2] = Promise.all([async1(), async2()])periksa cuplikan kode berikut

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

Haven
sumber
11
Ini terlihat seperti pilihan yang lebih baik bagi saya daripada Promise.all - dan dengan destrukturisasi tugas Anda bahkan dapat melakukan [someResult, anotherResult] = [await someResult, await anotherResult]jika Anda mengubah constke let.
jawj
28
Tapi ini masih mengeksekusi awaitpernyataan berurutan, kan? Artinya, eksekusi berhenti sampai yang pertama awaitselesai, lalu pindah ke yang kedua. Promise.alldijalankan secara paralel.
Andru
8
@Haven terima kasih. Ini harus menjadi jawaban yang diterima.
Stefan D
87
Jawaban ini menyesatkan karena fakta bahwa kedua menunggu dilakukan di baris yang sama tidak relevan. Yang penting adalah bahwa dua panggilan async dilakukan sebelum keduanya ditunggu.
JohnnyHK
15
@Haven solusi ini tidak sama dengan Promise.all. Jika setiap permintaan adalah panggilan jaringan, await someResultharus diselesaikan sebelum await anotherResultdimulai. Sebaliknya, dalam Promise.alldua awaitpanggilan dapat dimulai sebelum salah satu diselesaikan.
Ben Winding
89

Memperbarui:

Jawaban asli menyulitkan (dan dalam beberapa kasus tidak mungkin) untuk menangani penolakan janji dengan benar. Solusi yang benar adalah dengan menggunakan Promise.all:

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

Jawaban asli:

Pastikan Anda memanggil kedua fungsi sebelum menunggu salah satunya:

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

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
sumber
1
@JeffFischer Saya menambahkan komentar yang diharapkan membuatnya lebih jelas.
Jonathan Potter
9
Saya merasa ini adalah jawaban yang paling murni
Gershom
1
Jawaban ini jauh lebih jelas daripada Haven. Jelas bahwa pemanggilan fungsi akan mengembalikan objek janji, dan awaitkemudian akan mengatasinya menjadi nilai aktual.
user1032613
3
Ini tampaknya bekerja sekilas, tetapi memiliki masalah mengerikan dengan penolakan yang tidak tertangani . Jangan gunakan ini!
Bergi
1
@Bergi Anda benar, terima kasih telah menunjukkannya! Saya telah memperbarui jawabannya dengan solusi yang lebih baik.
Jonathan Potter
24

Ada cara lain tanpa Promise.all () untuk melakukannya secara paralel:

Pertama, kami memiliki 2 fungsi untuk mencetak angka:

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

Ini berurutan:

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

Ini paralel:

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

Ini dapat dilakukan dengan Promise.allSettled () , yang mirip dengan Promise.all()tetapi tanpa perilaku gagal-cepat.

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!"}

Catatan : Ini adalah fitur tepi berdarah dengan dukungan browser terbatas, jadi saya sangat menyarankan untuk memasukkan polyfill untuk fungsi ini.

Jonathan Sudiaman
sumber
7

Saya telah membuat intisari pengujian beberapa cara berbeda untuk menyelesaikan janji, dengan hasil. Mungkin bermanfaat untuk melihat opsi yang berfungsi.

SkarXa
sumber
Tes 4 dan 6 di intinya mengembalikan hasil yang diharapkan. Lihat stackoverflow.com/a/42158854/5683904 oleh NoNameProvided yang menjelaskan perbedaan antara opsi.
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
    */

Meskipun pengaturan p1, p2 dan p3 tidak sepenuhnya menjalankannya secara paralel, mereka tidak menahan eksekusi apa pun dan Anda dapat menjebak kesalahan kontekstual dengan tangkapan.

Thrunobulax
sumber
2
Selamat datang di Stack Overflow. Meskipun kode Anda dapat memberikan jawaban atas pertanyaan, tambahkan konteks di sekitarnya sehingga orang lain akan mengetahui apa yang dilakukannya dan mengapa ada di sana.
Theo
1

Dalam kasus saya, saya memiliki beberapa tugas yang ingin saya jalankan secara paralel, tetapi saya perlu melakukan sesuatu yang berbeda dengan hasil dari tugas-tugas itu.

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

Dan hasilnya:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
sumber
keren untuk kreasi dinamis (berbagai sumber daya)
Michal Miky Jankovský
1

menunggu Promise.all ([someCall (), anotherCall ()]); seperti yang sudah disebutkan akan bertindak sebagai pagar benang (sangat umum dalam kode paralel sebagai CUDA), maka itu akan memungkinkan semua janji di dalamnya untuk berjalan tanpa saling menghalangi, tetapi akan mencegah eksekusi untuk melanjutkan sampai SEMUA diselesaikan.

pendekatan lain yang layak untuk dibagikan adalah Node.js async yang juga akan memungkinkan Anda untuk dengan mudah mengontrol jumlah konkurensi yang biasanya diinginkan jika tugas terkait langsung dengan penggunaan sumber daya terbatas seperti panggilan API, operasi I / O, dll.

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

Kredit untuk artikel Sedang autor ( baca selengkapnya )

Thiago Conrado
sumber
-5

Saya memilih:

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

Waspadai saat Anda memanggil fungsi, itu dapat menyebabkan hasil yang tidak terduga:

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

Namun mengikuti selalu memicu permintaan untuk membuat Pengguna baru

// 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
sumber
Karena Anda mendeklarasikan fungsi di luar / sebelum pengujian kondisi, dan memanggilnya. Coba balut mereka di elseblok.
Haven
@Haven: Maksud saya ketika Anda memisahkan momen yang Anda panggil fungsi vs menunggu dapat menyebabkan hasil yang tidak terduga, misalnya: permintaan HTTP async.
Hoang Le Anh Tu
-6

Saya membuat fungsi pembantu waitAll, mungkin itu bisa membuatnya lebih manis. Ini hanya berfungsi di nodejs untuk saat ini, bukan di browser chrome.

    //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
sumber
3
Tidak, paralelisasi tidak terjadi sama sekali di sini. The forLoop berurutan menanti setiap janji dan menambahkan hasilnya ke array.
Szczepan Hołyszewski
Saya mengerti ini sepertinya tidak bekerja untuk orang. Jadi saya menguji di node.js dan browser. Tes dilewatkan dalam node.js (v10, v11), firefox, itu tidak berfungsi di browser chrome. Kasing
Fred Yang
2
Saya menolak untuk percaya ini. Tidak ada dalam standar yang mengatakan iterasi yang berbeda dari for for dapat secara otomatis diparalelkan; ini bukan cara kerja javascript. Cara kode loop ditulis, ini berarti ini: "menunggu satu item (menunggu expr), MAKA hasil push untuk temp, MAKA mengambil item berikutnya (iterasi selanjutnya dari for loop)." Menunggu "untuk setiap item benar-benar terbatas pada satu pengulangan loop. Jika tes menunjukkan bahwa ada paralelisasi, itu pasti karena transpiler melakukan sesuatu yang tidak standar atau buggy.
Szczepan Hołyszewski
@ SzczepanHołyszewski Kepercayaan Anda terhadap ketidakmampuan tanpa menjalankan test case menginspirasi saya untuk melakukan beberapa rename dan tambahan komentar. Semua kode adalah ES6 tua biasa, tidak perlu transpiling.
Fred Yang
Tidak yakin mengapa hal ini dibatalkan begitu banyak. Ini pada dasarnya jawaban yang sama yang diberikan @ user2883596.
Jonathan Sudiaman