Apakah Node.js asli Promise.all memproses secara paralel atau berurutan?

173

Saya ingin memperjelas hal ini, karena dokumentasinya tidak terlalu jelas tentang hal itu;

T1: Apakah Promise.all(iterable)memproses semua janji secara berurutan atau paralel? Atau, lebih khusus, apakah itu setara dengan menjalankan janji dirantai seperti

p1.then(p2).then(p3).then(p4).then(p5)....

atau itu beberapa jenis lain dari algoritma di mana semua p1, p2, p3, p4, p5, dll dipanggil pada waktu yang sama (secara paralel) dan hasilnya dikembalikan segera setelah semua tekad (atau satu menolak)?

T2: Jika Promise.allberjalan secara paralel, apakah ada cara yang nyaman untuk menjalankan iterable secara berurutan?

Catatan : Saya tidak ingin menggunakan Q, atau Bluebird, tetapi semua spesifikasi ES6 asli.

Yanick Rochon
sumber
Apakah Anda bertanya tentang implementasi node (V8), atau tentang spesifikasi?
Amit
1
Saya cukup yakin Promise.allmengeksekusi mereka secara paralel.
royhowie
@Amit, saya menandai node.jsdan io.jskarena ini adalah tempat saya menggunakannya. Jadi, ya, implementasi V8 jika Anda mau.
Yanick Rochon
9
Janji tidak bisa "dieksekusi". Mereka memulai tugas mereka ketika sedang dibuat - mereka hanya mewakili hasil - dan Anda mengeksekusi semuanya secara paralel bahkan sebelum meneruskannya Promise.all.
Bergi
Janji dieksekusi pada saat penciptaan. (dapat dikonfirmasi dengan menjalankan sedikit kode). Dalam new Promise(a).then(b); c();a dieksekusi pertama, lalu c, lalu b. Bukan Janji. Semua yang menjalankan janji-janji ini, itu hanya menangani ketika mereka menyelesaikan.
Mateon1

Jawaban:

257

Apakah Promise.all(iterable)melaksanakan semua janji?

Tidak, janji tidak bisa "dieksekusi". Mereka memulai tugas mereka ketika sedang dibuat - mereka hanya mewakili hasil - dan Anda mengeksekusi semuanya secara paralel bahkan sebelum meneruskannya Promise.all.

Promise.alltidak hanya menunggu beberapa janji. Tidak peduli urutan apa yang mereka selesaikan, atau apakah perhitungannya berjalan secara paralel.

apakah ada cara mudah untuk menjalankan iterable secara berurutan?

Jika Anda sudah memiliki janji, Anda tidak bisa berbuat banyak tetapi Promise.all([p1, p2, p3, …])(yang tidak memiliki urutan). Tetapi jika Anda memang memiliki iterable fungsi asinkron, Anda memang dapat menjalankannya secara berurutan. Pada dasarnya Anda harus mendapatkannya

[fn1, fn2, fn3, …]

untuk

fn1().then(fn2).then(fn3).then(…)

dan solusi untuk melakukannya adalah menggunakan Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())
Bergi
sumber
1
Dalam contoh ini, apakah iterable array fungsi yang mengembalikan janji yang ingin Anda panggil?
James Reategui
2
@ SHSHi ini: Persis seperti thenurutan - nilai kembali adalah janji untuk fnhasil terakhir , dan Anda dapat membuat panggilan balik lainnya ke sana.
Bergi
1
@wojja Itu persis sama dengan fn1().then(p2).then(fn3).catch(…? Tidak perlu menggunakan ekspresi fungsi.
Bergi
1
@wojja Tentu saja retValFromF1dilewatkan ke p2, itulah yang p2dilakukannya. Tentu, jika Anda ingin melakukan lebih banyak (melewati variabel tambahan, memanggil beberapa fungsi, dll) Anda perlu menggunakan ekspresi fungsi, meskipun mengubah p2dalam array akan lebih mudah
Bergi
1
@ robe007 Ya, yang saya maksud iterableadalah [fn1, fn2, fn3, …]array
Bergi
62

Sejajar

await Promise.all(items.map(async item => { await fetchItem(item) }))

Keuntungan: Lebih cepat. Semua iterasi akan dieksekusi bahkan jika salah satu gagal.

Berurutan

for (let i = 0; i < items.length; i++) {
    await fetchItem(items[i])
}

Keuntungan: Variabel dalam loop dapat dibagi oleh setiap iterasi. Berperilaku seperti kode sinkron imperatif normal.

david_adler
sumber
7
Atau:for (const item of items) await fetchItem(item);
Robert Penner
1
@david_adler Sebagai contoh paralel, keuntungan yang Anda katakan Semua iterasi akan dieksekusi meskipun salah satu gagal . Jika saya tidak salah ini akan tetap gagal dengan cepat. Untuk mengubah perilaku ini orang dapat melakukan sesuatu seperti: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor
@Taimoor ya itu "gagal cepat" dan melanjutkan mengeksekusi kode setelah Janji. Semua tetapi semua iterasi masih dieksekusi codepen.io/mfbx9da4/pen/BbaaXr
david_adler
Pendekatan ini lebih baik, ketika asyncfungsinya adalah panggilan API dan Anda tidak ingin DDOS server. Anda memiliki kontrol yang lebih baik atas hasil individual dan kesalahan yang dilemparkan dalam eksekusi. Bahkan lebih baik Anda dapat memutuskan kesalahan apa untuk melanjutkan dan apa yang harus dilakukan.
mandarin
Perhatikan bahwa javascript sebenarnya tidak mengeksekusi permintaan asinkron dalam "paralel" menggunakan utas karena javascript adalah utas tunggal. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler
11

Jawaban Bergis membuat saya di jalur yang benar menggunakan Array.reduce.

Namun, untuk benar-benar mendapatkan fungsi mengembalikan janji saya untuk mengeksekusi satu demi satu, saya harus menambahkan beberapa lagi bersarang.

Kasing asli saya adalah array file yang harus saya transfer satu demi satu karena keterbatasan downstream ...

Inilah yang akhirnya saya dapatkan.

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(() => {
            return transferFile(theFile); //function returns a promise
        });
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

Seperti yang disarankan oleh jawaban sebelumnya, gunakan:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(transferFile(theFile));
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

tidak menunggu untuk menyelesaikan transfer sebelum memulai yang lain dan juga teks "Semua file yang ditransfer" datang bahkan sebelum transfer file pertama dimulai.

Tidak yakin apa yang saya lakukan salah, tetapi ingin berbagi apa yang berhasil untuk saya.

Sunting: Sejak saya menulis posting ini, saya sekarang mengerti mengapa versi pertama tidak berfungsi. lalu () mengharapkan fungsi mengembalikan janji. Jadi, Anda harus memasukkan nama fungsi tanpa tanda kurung! Sekarang, fungsi saya ingin argumen jadi saya perlu membungkus dalam fungsi anonim tanpa mengambil argumen!

tkarl
sumber
4

hanya untuk menguraikan jawaban @ Bergi (yang sangat ringkas, tetapi sulit dipahami;)

Kode ini akan menjalankan setiap item dalam array dan menambahkan 'rantai kemudian' berikutnya ke akhir;

function eachorder(prev,order) {
        return prev.then(function() {
          return get_order(order)
            .then(check_order)
            .then(update_order);
        });
    }
orderArray.reduce(eachorder,Promise.resolve());

harapan itu masuk akal.

TimoSolo
sumber
3

Anda juga dapat memproses iterable secara berurutan dengan fungsi async menggunakan fungsi rekursif. Misalnya, diberikan array auntuk diproses dengan fungsi asinkron someAsyncFunction():

var a = [1, 2, 3, 4, 5, 6]

function someAsyncFunction(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("someAsyncFunction: ", n)
      resolve(n)
    }, Math.random() * 1500)
  })
}

//You can run each array sequentially with: 

function sequential(arr, index = 0) {
  if (index >= arr.length) return Promise.resolve()
  return someAsyncFunction(arr[index])
    .then(r => {
      console.log("got value: ", r)
      return sequential(arr, index + 1)
    })
}

sequential(a).then(() => console.log("done"))

Mark Meyer
sumber
menggunakan array.prototype.reducejauh lebih baik dalam hal kinerja daripada fungsi rekursif
Mateusz Sowiński
@ MateuszSowiński, ada batas waktu 1500ms antara setiap panggilan. Menimbang bahwa ini melakukan panggilan async secara berurutan, sulit untuk melihat bagaimana itu relevan bahkan untuk perputaran async yang sangat cepat.
Mark Meyer
Katakanlah Anda harus menjalankan 40 fungsi async yang sangat cepat satu sama lain - menggunakan fungsi rekursif akan menyumbat memori Anda dengan cukup cepat
Mateusz Sowiński
@ MateuszSowiński, bahwa tumpukan tidak berakhir di sini ... kami akan kembali setelah setiap panggilan. Bandingkan dengan di reducemana Anda harus membangun seluruh then()rantai dalam satu langkah dan kemudian jalankan.
Mark Meyer
Dalam panggilan ke-40 dari fungsi sekuensial, panggilan pertama dari fungsi tersebut masih dalam memori menunggu rantai fungsi sekuensial kembali
Mateusz Sowiński
3

NodeJS tidak menjalankan janji secara paralel, ia menjalankannya secara bersamaan karena ini adalah arsitektur loop peristiwa tunggal berulir. Ada kemungkinan untuk menjalankan berbagai hal secara paralel dengan menciptakan proses anak baru untuk memanfaatkan CPU multi-core.

Parallel Vs Concurent

Pada kenyataannya, apa yang Promise.alldilakukan adalah, menumpuk fungsi janji dalam antrian yang sesuai (lihat arsitektur loop acara) menjalankannya secara bersamaan (panggilan P1, P2, ...) kemudian menunggu setiap hasil, kemudian menyelesaikan Janji. Semua dengan semua janji hasil. Promise.all akan gagal pada janji pertama yang gagal, kecuali jika Anda sendiri yang mengelola penolakannya.

Ada perbedaan besar antara paralel dan konkuren, yang pertama akan menjalankan perhitungan yang berbeda dalam proses yang terpisah pada waktu yang sama persis dan mereka akan berkembang di sana, sementara yang lain akan menjalankan perhitungan yang berbeda satu demi satu tanpa menunggu yang sebelumnya. perhitungan untuk menyelesaikan dan maju pada saat yang sama tanpa tergantung satu sama lain

Akhirnya, untuk menjawab pertanyaan Anda, Promise.alltidak akan mengeksekusi baik secara paralel atau berurutan tetapi bersamaan.

Adrien De Peretti
sumber
Ini tidak benar. NodeJS dapat menjalankan berbagai hal secara paralel. NodeJS memiliki konsep utas pekerja. Secara default jumlah utas pekerja adalah 4. Misalnya, jika Anda menggunakan pustaka crypto untuk hash dua nilai maka Anda dapat mengeksekusinya secara paralel. Dua utas pekerja akan menangani tugas. Tentu saja, CPU Anda harus multi-core untuk mendukung paralelisme.
Shihab
Ya Anda benar, itulah yang saya katakan di akhir paragraf pertama, tetapi saya berbicara tentang proses anak, tentu saja mereka dapat menjalankan pekerja.
Adrien De Peretti
2

Menggunakan async menunggu berbagai janji dapat dengan mudah dieksekusi secara berurutan:

let a = [promise1, promise2, promise3];

async function func() {
  for(let i=0; i<a.length; i++){
    await a[i]();
  }  
}

func();

Catatan: Dalam implementasi di atas, jika janji ditolak, sisanya tidak akan dieksekusi. Jika Anda ingin semua janji Anda dieksekusi, maka bungkus await a[i]();bagian dalam Andatry catch

Ayan
sumber
2

paralel

lihat contoh ini

const resolveAfterTimeout = async i => {
  return new Promise(resolve => {
    console.log("CALLED");
    setTimeout(() => {
      resolve("RESOLVED", i);
    }, 5000);
  });
};

const call = async () => {
  const res = await Promise.all([
    resolveAfterTimeout(1),
    resolveAfterTimeout(2),
    resolveAfterTimeout(3),
    resolveAfterTimeout(4),
    resolveAfterTimeout(5),
    resolveAfterTimeout(6)
  ]);
  console.log({ res });
};

call();

dengan menjalankan kode itu akan menghibur "DISEBUT" untuk semua enam janji dan ketika mereka diselesaikan itu akan menghibur setiap 6 tanggapan setelah batas waktu pada waktu yang sama

Chintan Rajpara
sumber
1

Jawaban Bergi membantu saya untuk membuat panggilan sinkron. Saya telah menambahkan contoh di bawah ini di mana kita memanggil setiap fungsi setelah fungsi sebelumnya dipanggil.

function func1 (param1) {
    console.log("function1 : " + param1);
}
function func2 () {
    console.log("function2");
}
function func3 (param2, param3) {
    console.log("function3 : " + param2 + ", " + param3);
}

function func4 (param4) {
    console.log("function4 : " + param4);
}
param4 = "Kate";

//adding 3 functions to array

a=[
    ()=>func1("Hi"),
    ()=>func2(),
    ()=>func3("Lindsay",param4)
  ];

//adding 4th function

a.push(()=>func4("dad"));

//below does func1().then(func2).then(func3).then(func4)

a.reduce((p, fn) => p.then(fn), Promise.resolve());
Nithi
sumber
Apakah ini jawaban untuk pertanyaan awal?
Giulio Caccin
0

Anda dapat melakukannya dengan for loop.

fungsi async mengembalikan janji

async function createClient(client) {
    return await Client.create(client);
}

let clients = [client1, client2, client3];

jika Anda menulis kode berikut maka klien dibuat paralel

const createdClientsArray = yield Promise.all(clients.map((client) =>
    createClient(client);
));

maka semua klien dibuat sejajar. tetapi jika Anda ingin membuat klien secara berurutan maka Anda harus menggunakan untuk loop

const createdClientsArray = [];
for(let i = 0; i < clients.length; i++) {
    const createdClient = yield createClient(clients[i]);
    createdClientsArray.push(createdClient);
}

maka semua klien dibuat secara berurutan.

selamat coding :)

Deepak Sisodiya
sumber
8
Pada saat ini, async/ awaithanya tersedia dengan transpiler, atau menggunakan mesin selain Node. Juga, Anda benar-benar tidak boleh bergaul asyncdengan yield. Di mana mereka bertindak sama dengan transpiler dan co, mereka benar-benar sangat berbeda dan seharusnya tidak saling menggantikan satu sama lain. Juga, Anda harus menyebutkan batasan ini karena jawaban Anda membingungkan bagi programmer pemula.
Yanick Rochon
0

Saya telah menggunakan untuk untuk menyelesaikan janji-janji berurutan. Saya tidak yakin apakah ini membantu di sini, tetapi inilah yang saya lakukan.

async function run() {
    for (let val of arr) {
        const res = await someQuery(val)
        console.log(val)
    }
}

run().then().catch()
Nick Kotenberg
sumber
0

ini mungkin menjawab sebagian dari pertanyaan Anda.

ya, Anda dapat rantai array janji mengembalikan fungsi sebagai berikut ... (ini meneruskan hasil dari masing-masing fungsi ke yang berikutnya). Anda tentu saja dapat mengeditnya untuk memberikan argumen yang sama (atau tanpa argumen) ke setiap fungsi.

function tester1(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a + 1);
    }, 1000);
  })
}

function tester2(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a * 5);
    }, 1000);
  })
}

function promise_chain(args, list, results) {

  return new Promise(function(done, errs) {
    var fn = list.shift();
    if (results === undefined) results = [];
    if (typeof fn === 'function') {
      fn(args).then(function(result) {
        results.push(result);
        console.log(result);
        promise_chain(result, list, results).then(done);
      }, errs);
    } else {
      done(results);
    }

  });

}

promise_chain(0, [tester1, tester2, tester1, tester2, tester2]).then(console.log.bind(console), console.error.bind(console));

cestmoi
sumber
0

Saya sengaja menemukan halaman ini ketika mencoba untuk memecahkan masalah di NodeJS: reassembly potongan file. Pada dasarnya: Saya memiliki berbagai nama file. Saya perlu menambahkan semua file itu, dalam urutan yang benar, untuk membuat satu file besar. Saya harus melakukan ini secara tidak sinkron.

Modul 'fs' Node memang menyediakan appendFileSync tapi saya tidak ingin memblokir server selama operasi ini. Saya ingin menggunakan modul fs.promises dan menemukan cara untuk menyatukan semua ini. Contoh-contoh pada halaman ini tidak cukup berhasil bagi saya karena saya benar-benar membutuhkan dua operasi: fsPromises.read () untuk membaca file chunk, dan fsPromises.appendFile () untuk menyesuaikan dengan file tujuan. Mungkin jika saya lebih baik dengan javascript saya bisa membuat jawaban sebelumnya berfungsi untuk saya. ;-)

Saya sengaja menemukan ini ... https://css-tricks.com/why-using-reduce-to- berikutnyaally- resolve-promises- works/ ... dan saya dapat meretas bersama solusi yang berfungsi.

TLDR:

/**
 * sequentially append a list of files into a specified destination file
 */
exports.append_files = function (destinationFile, arrayOfFilenames) {
    return arrayOfFilenames.reduce((previousPromise, currentFile) => {
        return previousPromise.then(() => {
            return fsPromises.readFile(currentFile).then(fileContents => {
                return fsPromises.appendFile(destinationFile, fileContents);
            });
        });
    }, Promise.resolve());
};

Dan inilah tes unit melati untuknya:

const fsPromises = require('fs').promises;
const fsUtils = require( ... );
const TEMPDIR = 'temp';

describe("test append_files", function() {
    it('append_files should work', async function(done) {
        try {
            // setup: create some files
            await fsPromises.mkdir(TEMPDIR);
            await fsPromises.writeFile(path.join(TEMPDIR, '1'), 'one');
            await fsPromises.writeFile(path.join(TEMPDIR, '2'), 'two');
            await fsPromises.writeFile(path.join(TEMPDIR, '3'), 'three');
            await fsPromises.writeFile(path.join(TEMPDIR, '4'), 'four');
            await fsPromises.writeFile(path.join(TEMPDIR, '5'), 'five');

            const filenameArray = [];
            for (var i=1; i < 6; i++) {
                filenameArray.push(path.join(TEMPDIR, i.toString()));
            }

            const DESTFILE = path.join(TEMPDIR, 'final');
            await fsUtils.append_files(DESTFILE, filenameArray);

            // confirm "final" file exists    
            const fsStat = await fsPromises.stat(DESTFILE);
            expect(fsStat.isFile()).toBeTruthy();

            // confirm content of the "final" file
            const expectedContent = new Buffer('onetwothreefourfive', 'utf8');
            var fileContents = await fsPromises.readFile(DESTFILE);
            expect(fileContents).toEqual(expectedContent);

            done();
        }
        catch (err) {
            fail(err);
        }
        finally {
        }
    });
});

Saya harap ini membantu seseorang.

Jay
sumber