การประมวลผล Promise.all ดั้งเดิมของ Node.js เป็นแบบขนานหรือเรียงตามลำดับหรือไม่


173

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

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

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

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

Q2:หากPromise.allทำงานแบบขนานมีวิธีที่สะดวกในการเรียกใช้ซ้ำ ๆ ได้หรือไม่?

หมายเหตุ : ฉันไม่ต้องการใช้ Q หรือ Bluebird แต่เป็นข้อมูลจำเพาะ ES6 ทั้งหมด


คุณถามเกี่ยวกับการนำ node (V8) ไปใช้หรือเกี่ยวกับ spec หรือไม่?
Amit

1
ฉันค่อนข้างแน่ใจว่าPromise.allรันมันแบบขนาน
royhowie

@ ยอมรับว่าฉันถูกตั้งค่าสถานะnode.jsและio.jsนี่คือที่ฉันใช้งาน ดังนั้นใช่การใช้งาน V8 หากคุณต้องการ
Yanick Rochon

9
คำสัญญาไม่สามารถ "ถูกดำเนินการ" พวกเขาเริ่มต้นงานของพวกเขาเมื่อพวกเขากำลังถูกสร้างขึ้น - พวกเขาเป็นตัวแทนผลเท่านั้น - และคุณPromise.allกำลังดำเนินการทุกอย่างในแบบคู่ขนานแม้กระทั่งก่อนที่จะผ่านพวกเขาไป
Bergi

สัญญาจะถูกดำเนินการในขณะที่สร้าง (สามารถยืนยันได้ด้วยการรันโค้ดสักครู่) ในnew Promise(a).then(b); c();a จะถูกดำเนินการก่อนจากนั้น c จากนั้น b ไม่ใช่สัญญาที่เรียกใช้สัญญาเหล่านี้ แต่จะจัดการเมื่อแก้ไข
Mateon1

คำตอบ:


257

กำลังPromise.all(iterable)ดำเนินการตามสัญญาทั้งหมดหรือไม่

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

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

มีวิธีที่สะดวกในการเรียกใช้ซ้ำได้หรือไม่

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

[fn1, fn2, fn3, …]

ถึง

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

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

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

1
ในตัวอย่างนี้เป็นอาร์เรย์ของฟังก์ชันที่คืนสัญญาที่คุณต้องการโทรหรือไม่
James Reategui

2
@SSHThis: มันเป็นไปthenตามลำดับ - ค่าส่งคืนคือสัญญาสำหรับfnผลลัพธ์สุดท้ายและคุณสามารถโยงการโทรกลับอื่น ๆ
Bergi

1
@wojjas มันเทียบเท่ากันแน่fn1().then(p2).then(fn3).catch(…? ไม่จำเป็นต้องใช้ฟังก์ชั่นการแสดงออก
Bergi

1
@wojjas แน่นอนว่าretValFromF1มันถูกส่งเข้าไปp2นั่นคือสิ่งที่p2ทำ แน่นอนว่าถ้าคุณต้องการทำมากกว่านี้ (ผ่านตัวแปรเพิ่มเติม, เรียกใช้หลายฟังก์ชัน, ฯลฯ ) คุณจำเป็นต้องใช้นิพจน์ฟังก์ชั่นแม้ว่าการเปลี่ยนค่าp2ในอาเรย์จะง่ายขึ้น
Bergi

1
@ robe007 ใช่ฉันหมายถึงนั่นiterableคือ[fn1, fn2, fn3, …]อาร์เรย์
Bergi

62

ในแบบคู่ขนาน

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

ข้อดี: เร็วกว่า การวนซ้ำทั้งหมดจะถูกดำเนินการแม้ว่าจะล้มเหลวก็ตาม

ในลำดับ

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

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


7
หรือ:for (const item of items) await fetchItem(item);
Robert Penner

1
@david_adler ในตัวอย่างข้อดีที่ขนานกันคุณกล่าวว่าการทำซ้ำทั้งหมดจะถูกดำเนินการแม้ว่าจะล้มเหลวก็ตาม ถ้าฉันไม่ผิดก็คงล้มเหลวเร็ว ในการเปลี่ยนพฤติกรรมนี้เราสามารถทำสิ่งต่าง ๆ เช่น: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor

@Taimoor ใช่มันไม่ "ล้มเหลวอย่างรวดเร็ว" และยังคงดำเนินการหลังจากที่รหัส Promise.all แต่ซ้ำทั้งหมดจะดำเนินการยังคงcodepen.io/mfbx9da4/pen/BbaaXr
david_adler

วิธีนี้จะดีกว่าเมื่อasyncฟังก์ชั่นคือการเรียก API และคุณไม่ต้องการ DDOS เซิร์ฟเวอร์ คุณสามารถควบคุมผลลัพธ์และข้อผิดพลาดแต่ละรายการได้ดีกว่าในการดำเนินการ ยิ่งไปกว่านั้นคุณสามารถตัดสินใจได้ว่าข้อผิดพลาดใดที่จะดำเนินการต่อไป
แมนดาริน

โปรดทราบว่าจาวาสคริปต์ไม่ได้ดำเนินการตามคำขอแบบอะซิงโครนัสใน "ขนาน" โดยใช้เธรดเนื่องจากจาวาสคริปต์เป็นเธรดเดี่ยว developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler

11

Bergis คำตอบให้ฉันในการติดตามขวาโดยใช้ 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);
});

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

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

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


4

เพื่ออธิบายรายละเอียดเกี่ยวกับคำตอบของ @ Bergi (ซึ่งรวบรัดมาก แต่ยังยากที่จะเข้าใจ)

รหัสนี้จะเรียกใช้แต่ละรายการในอาร์เรย์และเพิ่มถัดไป 'แล้วโซ่' ไปยังจุดสิ้นสุด;

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

หวังว่าจะสมเหตุสมผล


3

นอกจากนี้คุณยังสามารถประมวลผลซ้ำตามลำดับด้วยฟังก์ชั่น async โดยใช้ฟังก์ชั่นซ้ำ ตัวอย่างเช่นกำหนดอาร์เรย์aให้ประมวลผลด้วยฟังก์ชันอะซิงโครนัส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"))


การใช้array.prototype.reduceดีกว่าในแง่ของประสิทธิภาพมากกว่าฟังก์ชั่นวนซ้ำ
Mateusz Sowiński

@ MateuszSowińskiมีการหมดเวลา 1500ms ระหว่างการโทรแต่ละครั้ง เมื่อพิจารณาว่าสิ่งนี้กำลังดำเนินการเรียก async ตามลำดับมันยากที่จะดูว่ามันเกี่ยวข้องกันอย่างไรแม้จะเป็นการตอบสนองแบบรวดเร็วของ async
Mark Meyer

สมมติว่าคุณต้องเรียกใช้ฟังก์ชั่น async แบบเร็ว 40 รายการหลังจากใช้ฟังก์ชั่นแบบเรียกซ้ำจะทำให้หน่วยความจำของคุณอุดตันเร็ว
Mateusz Sowiński

@ MateuszSowińskiว่าสแต็คไม่ได้อยู่ที่นี่ ... เรากลับมาทุกครั้ง เปรียบเทียบกับreduceที่ที่คุณต้องสร้างthen()สายโซ่ทั้งหมดในขั้นตอนเดียวแล้วดำเนินการ
Mark Meyer

ในการเรียกฟังก์ชั่นลำดับที่ 40 การโทรครั้งแรกของฟังก์ชันยังคงอยู่ในหน่วยความจำที่รอฟังก์ชั่นการเรียงลำดับเพื่อกลับมา
Mateusz Sowiński

2

การใช้async กำลังรออาร์เรย์ของสัญญาที่สามารถดำเนินการได้อย่างต่อเนื่อง:

let a = [promise1, promise2, promise3];

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

func();

หมายเหตุ: ในการดำเนินการข้างต้นหากสัญญาถูกปฏิเสธส่วนที่เหลือจะไม่ executed.If คุณต้องการทุกสัญญาของคุณที่จะดำเนินการแล้วห่อของคุณawait a[i]();ภายในtry catch


2

ขนาน

ดูตัวอย่างนี้

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

โดยเรียกใช้รหัสมันจะคอนโซล "เรียกว่า" สำหรับทั้งหกสัญญาและเมื่อพวกเขาได้รับการแก้ไขมันจะปลอบใจทุก ๆ 6 คำตอบหลังจากหมดเวลาในเวลาเดียวกัน


2

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

เทียบเคียงกับขนาน

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

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

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


นี้ไม่ถูกต้อง. NodeJS สามารถเรียกใช้สิ่งต่าง ๆ ในแบบคู่ขนาน NodeJS มีแนวคิดของเธรดผู้ปฏิบัติงาน โดยค่าเริ่มต้นจำนวนเธรดผู้ปฏิบัติงานคือ 4 ตัวอย่างเช่นถ้าคุณใช้ไลบรารี crypto เพื่อแฮชสองค่าจากนั้นคุณสามารถเรียกใช้งานแบบขนาน สองเธรดผู้ปฏิบัติงานจะจัดการงาน แน่นอน CPU ของคุณต้องเป็นแบบมัลติคอร์เพื่อรองรับการขนาน
Shihab

ใช่คุณพูดถูกในตอนท้ายของย่อหน้าแรก แต่ฉันพูดเกี่ยวกับกระบวนการเด็กแน่นอนว่าพวกเขาสามารถทำงานได้
Adrien De Peretti

1

คำตอบของ Bergi ช่วยให้ฉันทำการโทรแบบซิงโครนัสฉันได้เพิ่มตัวอย่างด้านล่างซึ่งเราเรียกแต่ละฟังก์ชั่นหลังจากเรียกฟังก์ชั่นก่อนหน้านี้

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

นี่เป็นคำตอบของคำถามต้นฉบับหรือไม่
Giulio Caccin

0

คุณสามารถทำได้โดยการวนซ้ำ

ฟังก์ชั่น async กลับมาสัญญา

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

let clients = [client1, client2, client3];

หากคุณเขียนรหัสต่อไปนี้ลูกค้าจะถูกสร้างขึ้นแบบคู่ขนาน

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

จากนั้นลูกค้าทั้งหมดจะถูกสร้างขึ้นแบบคู่ขนาน แต่ถ้าคุณต้องการสร้างไคลเอนต์ตามลำดับคุณควรใช้ลูป

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

จากนั้นลูกค้าทั้งหมดจะถูกสร้างขึ้นตามลำดับ

การเข้ารหัสที่มีความสุข :)


8
ในเวลานี้async/ awaitใช้ได้เฉพาะกับ transpiler หรือใช้เอ็นจินอื่นที่ไม่ใช่โหนด นอกจากนี้คุณมันไม่ควรผสมกับasync yieldเมื่อพวกเขาทำหน้าที่เดียวกันกับ transpiler และcoพวกเขาแตกต่างกันมากและไม่ควร substitude ซึ่งกันและกัน นอกจากนี้คุณควรพูดถึงข้อ จำกัด เหล่านี้เนื่องจากคำตอบของคุณทำให้นักเขียนมือใหม่สับสน
Yanick Rochon

0

ฉันใช้เพื่อแก้ไขสัญญาที่ต่อเนื่องกัน ฉันไม่แน่ใจว่ามันช่วยที่นี่ แต่นี่คือสิ่งที่ฉันได้ทำ

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

run().then().catch()

0

นี่อาจตอบคำถามของคุณบางส่วน

ใช่คุณสามารถโยงฟังก์ชันการส่งคืนสัญญาได้ดังนี้ ... (สิ่งนี้จะส่งผ่านผลลัพธ์ของแต่ละฟังก์ชันไปยังถัดไป) แน่นอนคุณสามารถแก้ไขได้เพื่อส่งผ่านอาร์กิวเมนต์เดียวกัน (หรือไม่มีอาร์กิวเมนต์) ไปยังแต่ละฟังก์ชัน

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


0

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

โมดูล 'fs' ของโหนดให้ appendFileSync แต่ฉันไม่ต้องการบล็อกเซิร์ฟเวอร์ในระหว่างการดำเนินการนี้ ฉันต้องการใช้โมดูล fs.promises และหาวิธีเชื่อมโยงสิ่งนี้เข้าด้วยกัน ตัวอย่างในหน้านี้ไม่ได้ผลสำหรับฉันเพราะฉันต้องการการดำเนินการสองอย่าง: fsPromises.read () เพื่ออ่านในไฟล์อันและ fsPromises.appendFile () เพื่อเชื่อมต่อกับไฟล์ปลายทาง บางทีถ้าฉันดีขึ้นด้วย javascript ฉันอาจทำให้คำตอบก่อนหน้านี้ใช้ได้สำหรับฉัน ;-)

ฉันพบสิ่งนี้ ... https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/ ... และฉันสามารถแฮ็คโซลูชันการทำงานร่วมกันได้

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

และนี่คือการทดสอบหน่วยจัสมินสำหรับมัน:

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

ฉันหวังว่ามันจะช่วยให้ใครบางคน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.