ค้นหาเว็บไซต์

javascript - การประมวลผล Promise.all ของ Node.js แบบเนทีฟเป็นแบบขนานหรือแบบเรียงลำดับ?


ฉันต้องการชี้แจงประเด็นนี้ เพราะเอกสารไม่ค่อยชัดเจนเกี่ยวกับเรื่องนี้

คำถาม 1: Promise.all(iterable) กำลังประมวลผลพรอมิสทั้งหมดแบบเรียงลำดับติดต่อกันหรือแบบขนาน? หรือพูดให้เจาะจงกว่านั้น มันเหมือนกับการรันพรอมิสที่เชื่อมต่อกันแบบ

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

หรือว่าเป็นอัลกอริธึมประเภทอื่นที่ทุก p1, p2, p3, p4, p5 เป็นต้น ถูกเรียกใช้งานพร้อมกัน (แบบขนาน) และผลลัพธ์จะถูกส่งกลับทันทีที่ทุกตัวทำงานเสร็จ (หรือมีตัวใดตัวหนึ่งถูกปฏิเสธ)?

คำถามที่ 2: ถ้า Promise.all ทำงานแบบขนาน มีวิธีที่สะดวกในการรัน iterable แบบเรียงลำดับไหม?

หมายเหตุ: ฉันไม่ต้องการใช้ Q หรือ Bluebird แต่ต้องการใช้เฉพาะสเปค ES6 ดั้งเดิมทั้งหมด

คำตอบ #1

Promise.all(iterable) กำลังรันทุก promise ใช่หรือไม่?

ไม่, สัญญาไม่สามารถ "ถูกดำเนินการ" ได้ เพราะมันไม่ได้ประกอบด้วยโค้ดที่สามารถรันได้ สัญญาถูก สร้าง เมื่อคุณเริ่มงาน และมันแสดงถึงผลลัพธ์เพียงอย่างเดียว เป็น โค้ดของคุณ ที่ทำการรันทุกอย่างพร้อมกัน โดยการเริ่มงานทั้งหมดในคราวเดียวเพื่อรันพร้อมกัน สิ่งนี้เกิดขึ้นแม้ก่อน (และก็จะเกิดขึ้นแม้ไม่มี) การส่งสัญญาไปยัง Promise.all

Promise.all ทำเพียง await คำสัญญาหลายรายการเท่านั้น มันไม่สนใจว่าคำตอบของงานเหล่านั้นจะได้รับการแก้ไขตามลำดับใด หรือการประมวลผลจะทำงานพร้อมกันหรือไม่

มีวิธีที่สะดวกในการรัน iterable แบบเรียงลำดับหรือไม่?

หากคุณมี promises ของคุณอยู่แล้ว คุณก็ทำอะไรได้ไม่มากนอกจาก Promise.all([p1, p2, p3, …]) (ซึ่งไม่มีแนวคิดของลำดับ) แต่ถ้าคุณมี iterable ของฟังก์ชันแบบอะซิงโครนัส คุณสามารถรันมันทีละขั้นได้จริง ๆ โดยพื้นฐานคุณจำเป็นต้องได้จาก

[fn1, fn2, fn3, …]

ถึง

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

และวิธีแก้ปัญหาในการทำเช่นนั้นคือการใช้ Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())

คำตอบ #2

แบบขนาน

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

ข้อดี: เร็วขึ้น การวนซ้ำทั้งหมดจะเริ่มขึ้นแม้ว่าจะมีการล้มเหลวในภายหลัง อย่างไรก็ตาม มันจะ "ล้มเหลวอย่างรวดเร็ว" ใช้ Promise.allSettled เพื่อทำให้การวนซ้ำทั้งหมดเสร็จสมบูรณ์พร้อมกันแม้ว่าบางส่วนจะเกิดข้อผิดพลาด ในทางเทคนิค นี่คือการเรียกใช้งานพร้อมกันไม่ใช่แบบขนาน

ตามลำดับ

for (const item of items) {
  await fetchItem(item)
}

ข้อดี: ตัวแปรในลูปสามารถแชร์ได้โดยแต่ละการวนซ้ำ ทำงานเหมือนโค้ดซิงโครนัสแบบปกติ

คำตอบ #3

NodeJS จะไม่รัน promises แบบขนาน แต่มันจะรันแบบพร้อมกันเพราะสถาปัตยกรรมของมันเป็น single-threaded event loop มีความเป็นไปได้ที่จะรันสิ่งต่าง ๆ แบบขนานโดยการสร้าง child process ใหม่เพื่อใช้ประโยชน์จาก CPU ที่มีหลายคอร์

คู่ขนาน กับ พร้อมกัน

ในความเป็นจริง สิ่งที่ Promise.all ทำก็คือ การจัดเรียงฟังก์ชัน promises ไว้ในคิวที่เหมาะสม (ดูสถาปัตยกรรม event loop) และรันพวกมันพร้อมกัน (เรียก P1, P2,...) จากนั้นรอผลลัพธ์แต่ละตัว แล้วจึงแก้ไข Promise.all ด้วยผลลัพธ์ของ promises ทั้งหมด Promise.all จะล้มเหลวเมื่อตัว promise แรกล้มเหลว เว้นแต่ว่าคุณจะจัดการกับการปฏิเสธด้วยตัวเอง

มีความแตกต่างที่สำคัญระหว่างแบบขนานและแบบร่วมกัน แบบแรกจะรันการคำนวณต่าง ๆ ในกระบวนการแยกต่างหากในเวลาเดียวกันพอดี และแต่ละกระบวนการจะดำเนินไปด้วยจังหวะของตัวเอง ขณะที่แบบหลังจะรันการคำนวณต่าง ๆ ทีละอย่างโดยไม่ต้องรอให้การคำนวณก่อนหน้านั้นเสร็จสิ้น และดำเนินไปพร้อมกันโดยไม่ขึ้นอยู่กับกันและกัน

สุดท้าย เพื่อตอบคำถามของคุณ Promise.all จะไม่ทำงานแบบขนานหรือแบบลำดับ แต่จะทำงานแบบพร้อมกัน

คำตอบ #4

คำตอบของ Bergi ทำให้ฉันเริ่มต้นถูกทางโดยใช้ Array.reduce

อย่างไรก็ตาม เพื่อให้ฟังก์ชันที่คืนสัญญาของฉันทำงานทีละตัว ฉันจึงต้องเพิ่มการซ้อนเพิ่มเติมเข้าไป

กรณีการใช้งานจริงของฉันคืออาร์เรย์ของไฟล์ที่ฉันจำเป็นต้องโอนตามลำดับทีละหนึ่งเนื่องจากข้อจำกัดด้านล่าง...

นี่คือสิ่งที่ฉันลงเอยด้วย:

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

ตามที่คำตอบก่อนหน้านี้แนะนำ การใช้:

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

ไม่ได้รอให้การโอนเสร็จสมบูรณ์ก่อนเริ่มการโอนอีกครั้ง และข้อความ "โอนไฟล์ทั้งหมดแล้ว" ปรากฏก่อนที่การโอนไฟล์แรกจะเริ่มด้วยซ้ำ

ไม่แน่ใจว่าฉันทำผิดอะไร แต่ต้องการจะแบ่งปันสิ่งที่ได้ผลสำหรับฉัน

แก้ไข: ตั้งแต่ฉันเขียนโพสต์นี้ ตอนนี้ฉันเข้าใจแล้วว่าทำไมเวอร์ชันแรกถึงใช้ไม่ได้ then() คาดหวัง ฟังก์ชัน ที่คืนค่า promise ดังนั้น คุณควรส่งชื่อฟังก์ชันโดยไม่ใช้วงเล็บ! ตอนนี้ ฟังก์ชันของฉันต้องการอาร์กิวเมนต์ ดังนั้นฉันจึงต้องห่อมันในฟังก์ชันนิรนามที่ไม่มีอาร์กิวเมนต์!

บทความที่เกี่ยวข้อง