เรียก async / ฟังก์ชั่นที่รอคอยในแบบคู่ขนาน


431

เท่าที่ฉันเข้าใจใน ES7 / ES2016 การใส่รหัสหลายรายการawaitจะทำงานคล้ายกับการผูกมัด.then()กับสัญญาซึ่งหมายความว่าพวกเขาจะดำเนินการอย่างใดอย่างหนึ่งหลังจากที่อื่นมากกว่าใน Parallerl ตัวอย่างเช่นเรามีรหัสนี้:

await someCall();
await anotherCall();

ฉันเข้าใจถูกต้องหรือไม่ว่าanotherCall()จะถูกเรียกเมื่อsomeCall()เสร็จสิ้นเท่านั้น อะไรคือวิธีที่สวยงามที่สุดในการโทรหาพวกเขาในแบบคู่ขนาน?

ฉันต้องการใช้มันในโหนดดังนั้นอาจมีวิธีแก้ปัญหาด้วยไลบรารี async หรือไม่?

แก้ไข: ฉันไม่พอใจกับวิธีแก้ปัญหาที่ให้ไว้ในคำถามนี้: การชะลอตัวเนื่องจากการรอคอยสัญญาแบบไม่ต่อเนื่องในเครื่องกำเนิดไฟฟ้า asyncเนื่องจากมันใช้เครื่องกำเนิดไฟฟ้าและฉันกำลังถามกรณีการใช้งานทั่วไปมากกว่า


1
@adeneo ไม่ถูกต้อง Javascript จะไม่ทำงานคู่ขนานภายในบริบทของตนเอง
Blindman67

5
@ Blindman67 - อย่างน้อยที่สุดวิธีที่ OP หมายถึงที่ซึ่งการทำงานของ async สองครั้งทำงานพร้อมกัน แต่ไม่ใช่ในกรณีนี้สิ่งที่ฉันตั้งใจจะเขียนก็คือพวกเขาทำงานในอนุกรมก่อนอื่นawaitจะรอให้ฟังก์ชันแรกเสร็จสมบูรณ์ ทั้งหมดก่อนที่จะดำเนินการที่สอง
adeneo

3
@ Blindman67 - เป็นเธรดเดี่ยว แต่ข้อ จำกัด นั้นไม่ได้ใช้กับวิธีการแบบอะซิงโครนัสพวกเขาสามารถเรียกใช้พร้อมกันและกลับการตอบสนองเมื่อพวกเขาเสร็จแล้วนั่นคือ OP หมายถึง "parallell"
adeneo

7
@ Blindman67 - ฉันคิดว่ามันค่อนข้างชัดเจนว่า OP ขออะไรการใช้รูปแบบ async / await จะทำให้ฟังก์ชั่นทำงานเป็นอนุกรมแม้ว่ามันจะเป็น async ดังนั้นครั้งแรกที่จะเสร็จสิ้นอย่างสมบูรณ์ก่อนที่สองจะถูกเรียกเป็นต้น OP คือ ถามวิธีการโทรทั้งสองฟังก์ชั่นในพารัลล์และอย่างชัดเจน async เป้าหมายคือการเรียกใช้พร้อมกันเช่นในพารัลล์เช่นทำสองคำขออาแจ็กซ์พร้อมกันซึ่งไม่เป็นปัญหาเลยใน javascript ตามที่คุณได้บันทึกไว้ให้รันโค้ดเนทีฟและใช้เธรดเพิ่มเติม
adeneo

3
@Bergi นี่ไม่ใช่คำถามที่เชื่อมโยงซ้ำ - นี่เป็นเรื่องเฉพาะเกี่ยวกับ async / await syntax และ native Promises คำถามที่เชื่อมโยงนั้นเกี่ยวกับไลบรารี่เบิร์ดเบิร์ดที่มีเครื่องกำเนิดและผลตอบแทน บางทีอาจคล้ายกับแนวคิด แต่ไม่ได้นำไปใช้
Iest

คำตอบ:


700

คุณสามารถรอที่Promise.all():

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

วิธีเก็บผลลัพธ์:

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

โปรดทราบว่าPromise.allล้มเหลวอย่างรวดเร็วซึ่งหมายความว่าทันทีที่สัญญาข้อใดข้อหนึ่งที่ให้ไว้นั้นถูกปฏิเสธแล้วสิ่งทั้งปวงก็ปฏิเสธ

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'

หาก Promise.allSettledแต่คุณต้องการที่จะรอสำหรับทุกสัญญาที่ให้ทั้งตอบสนองหรือปฏิเสธแล้วคุณสามารถใช้ โปรดทราบว่า Internet Explorer ไม่สนับสนุนวิธีการนี้

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


78
ทำความสะอาด แต่ระวังพฤติกรรมที่ล้มเหลวอย่างรวดเร็วของ Promise.all หากฟังก์ชันใด ๆ เกิดข้อผิดพลาด Promise.all จะปฏิเสธ
NoNameProvided

11
คุณสามารถจัดการผลลัพธ์บางส่วนได้ด้วย async / await ดูstackoverflow.com/a/42158854/2019689
NoName มีให้

131
เคล็ดลับสำหรับมืออาชีพ: ใช้การทำลายอาร์เรย์เพื่อเริ่มต้นจำนวนผลลัพธ์โดยพลการจาก Promise.all () เช่น:[result1, result2] = Promise.all([async1(), async2()]);
jonny

10
@jonny เรื่องนี้ล้มเหลวหรือไม่ นอกจากนี้หนึ่งจะยังคงต้อง= await Promise.all?
theUtherSide

5
@TheUtherSide คุณพูดถูก - ฉันไม่สนใจที่จะรวมการรอคอย
jonny

114

TL; DR

ใช้Promise.allสำหรับการเรียกฟังก์ชั่นแบบขนานพฤติกรรมการตอบรับไม่ถูกต้องเมื่อเกิดข้อผิดพลาด


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

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

ตัวอย่าง JSbin: http://jsbin.com/xerifanima/edit?js,console

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


ปรับปรุง:คำตอบนี้มีระยะเวลาที่แตกต่างกันในการจัดการข้อผิดพลาดตามที่คำตอบของ @ bergiก็ไม่ไม่โยนออกข้อผิดพลาดเป็นข้อผิดพลาดเกิดขึ้น แต่หลังจากที่สัญญาทั้งหมดจะดำเนินการ ฉันเปรียบเทียบผลลัพธ์กับเคล็ดลับของ @ jonny: [result1, result2] = Promise.all([async1(), async2()])ตรวจสอบข้อมูลโค้ดต่อไปนี้

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


11
ดูเหมือนจะเป็นตัวเลือกที่ดีกว่ามากที่จะฉันกว่า Promise.all นี้ - และมีการมอบหมาย destructuring คุณยังสามารถทำ[someResult, anotherResult] = [await someResult, await anotherResult]ถ้าคุณเปลี่ยนไปconst let
jawj

28
แต่สิ่งนี้ยังคงดำเนินการawaitตามลำดับอย่างถูกต้องใช่ไหม นั่นคือการดำเนินการหยุดชั่วคราวจนกว่าawaitจะแก้ไขครั้งแรกจากนั้นย้ายไปยังที่สอง Promise.allดำเนินการในแบบคู่ขนาน
Andru

8
ขอบคุณ @Haven นี่ควรเป็นคำตอบที่ยอมรับได้
สเตฟาน D

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

15
@Haven Promise.allแก้ปัญหานี้ไม่ได้เช่นเดียวกับ หากคำขอแต่ละรายการเป็นการเรียกเครือข่ายawait someResultจะต้องแก้ไขก่อนที่await anotherResultจะเริ่มต้น ในทางกลับกันในPromise.allการawaitโทรทั้งสองสามารถเริ่มต้นได้ก่อนที่จะมีการแก้ไข
Ben Winding

89

ปรับปรุง:

คำตอบเดิมทำให้ยาก (และในบางกรณีเป็นไปไม่ได้) ที่จะจัดการกับการปฏิเสธสัญญาอย่างถูกต้อง ทางออกที่ถูกต้องคือการใช้Promise.all:

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

คำตอบเดิม:

เพียงให้แน่ใจว่าคุณเรียกฟังก์ชั่นทั้งสองก่อนที่จะรออย่างใดอย่างหนึ่ง:

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

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;

1
@JeffFischer ฉันได้เพิ่มความคิดเห็นซึ่งหวังว่าจะทำให้ชัดเจนขึ้น
Jonathan Potter

9
ฉันรู้สึกว่านี่เป็นคำตอบที่บริสุทธิ์ที่สุดอย่างแน่นอน
Gershom

1
คำตอบนี้ชัดเจนกว่าของ Haven มาก เป็นที่ชัดเจนว่าการเรียกใช้ฟังก์ชันจะส่งคืนวัตถุสัญญาและawaitจากนั้นจะแก้ไขให้เป็นค่าจริง
user1032613

3
นี้จะปรากฏขึ้นในการทำงานได้อย่างรวดเร็วคร่าวๆ แต่มีปัญหาที่น่ากลัวกับการถูกปฏิเสธไม่สามารถจัดการได้ อย่าใช้สิ่งนี้!
Bergi

1
@Bergi ถูกต้องขอบคุณที่ชี้ให้เห็น! ฉันได้อัพเดตคำตอบด้วยวิธีแก้ปัญหาที่ดีกว่าแล้ว
Jonathan Potter

24

มีวิธีอื่นที่ไม่มี Promise.all () ทำแบบขนาน:

อันดับแรกเรามี 2 ฟังก์ชันเพื่อพิมพ์หมายเลข:

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

นี่คือลำดับ:

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

นี่คือขนาน:

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

10

ซึ่งสามารถทำได้ด้วยPromise.allSettled ()ซึ่งคล้ายกับPromise.all()แต่ไม่มีลักษณะการทำงานที่ล้มเหลวอย่างรวดเร็ว

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

หมายเหตุ : นี่เป็นคุณสมบัติที่มีเลือดออกที่มีการสนับสนุนเบราว์เซอร์ที่ จำกัด ดังนั้นฉันขอแนะนำอย่างยิ่งให้รวม polyfill สำหรับฟังก์ชั่นนี้


7

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


การทดสอบ 4 และ 6 ในส่วนสำคัญจะส่งกลับผลลัพธ์ที่คาดหวัง ดูstackoverflow.com/a/42158854/5683904โดย NoNameProvided ผู้อธิบายความแตกต่างระหว่างตัวเลือก
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
    */

ในขณะที่การตั้งค่า p1, p2 และ p3 นั้นไม่ได้รันคู่ขนานอย่างเคร่งครัด แต่พวกเขาจะไม่ทำการประมวลผลใด ๆ และคุณสามารถดักจับข้อผิดพลาดทางบริบทด้วยการจับ


2
ยินดีต้อนรับสู่ Stack Overflow ในขณะที่รหัสของคุณอาจให้คำตอบสำหรับคำถามโปรดเพิ่มบริบทรอบ ๆ เพื่อให้ผู้อื่นมีความคิดว่ามันทำอะไรและทำไมมันถึงมี
Theo

1

ในกรณีของฉันฉันมีงานหลายอย่างที่ฉันต้องการทำงานแบบขนาน แต่ฉันต้องทำสิ่งที่แตกต่างกับผลลัพธ์ของงานเหล่านั้น

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

และผลลัพธ์:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done

เด็ดสำหรับการสร้างแบบไดนามิก (ทรัพยากรมากมาย)
Michal Miky Jankovský

1

รอ Promise.all ([someCall (), anotherCall ()]); ดังที่ได้กล่าวมาแล้วจะทำหน้าที่เป็นรั้วด้าย (ทั่วไปมากในขนานรหัสเป็น CUDA) ดังนั้นมันจะช่วยให้สัญญาทั้งหมดในมันทำงานโดยไม่ปิดกั้นซึ่งกันและกัน แต่จะป้องกันไม่ให้การดำเนินการต่อไปจนกว่าจะได้รับการแก้ไขทั้งหมด

อีกวิธีหนึ่งที่คุ้มค่าที่จะแบ่งปันคือ Node.js async ซึ่งจะช่วยให้คุณสามารถควบคุมปริมาณการทำงานพร้อมกันที่มักจะต้องการได้อย่างง่ายดายหากงานเชื่อมโยงโดยตรงกับการใช้ทรัพยากรที่ จำกัด เช่นการเรียก API การดำเนินงาน I / O เป็นต้น

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

มอบเครดิตให้แก่ผู้เขียนบทความระดับกลาง ( อ่านเพิ่มเติม )


-5

ฉันลงคะแนนให้:

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

ระวังในขณะที่คุณเรียกฟังก์ชั่นมันอาจทำให้เกิดผลลัพธ์ที่ไม่คาดคิด:

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

แต่การทำตามจะทำให้คำขอสร้างผู้ใช้ใหม่เสมอ

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

เนื่องจากคุณประกาศฟังก์ชั่นนอก / ก่อนการทดสอบสภาพและเรียกพวกเขา ลองใส่ในelseบล็อก
Haven

@Haven: ฉันหมายถึงเมื่อคุณแยกช่วงเวลาที่คุณเรียกใช้ฟังก์ชั่นเทียบกับการรอคอยอาจนำไปสู่ผลลัพธ์ที่ไม่คาดคิดตัวอย่างเช่นคำขอ async HTTP
Hoang Le Anh Tu

-6

ฉันสร้างฟังก์ชั่นผู้ช่วย waitAll อาจเป็นเพราะมันสามารถทำให้หวานขึ้นได้ ใช้งานได้เฉพาะในnodejsเท่านั้นไม่ใช่ใน Chrome 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());

3
ไม่การขนานไม่เกิดขึ้นที่นี่เลย การforวนซ้ำจะรอแต่ละสัญญาและเพิ่มผลลัพธ์ลงในอาร์เรย์
Szczepan Hołyszewski

ฉันเข้าใจว่าสิ่งนี้ดูเหมือนจะไม่ทำงานสำหรับคน ดังนั้นฉันทดสอบใน node.js และเบราว์เซอร์ การทดสอบถูกส่งผ่านใน node.js (v10, v11), firefox มันไม่ทำงานในเบราว์เซอร์ chrome กรณีทดสอบอยู่ในgist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang

2
ฉันปฏิเสธที่จะเชื่อสิ่งนี้ ไม่มีอะไรในมาตรฐานที่บอกว่าการวนซ้ำที่แตกต่างกันของ for loop สามารถทำให้เป็นเส้นขนานโดยอัตโนมัติ นี่ไม่ใช่จาวาสคริปต์ วิธีเขียนรหัสการวนซ้ำหมายความว่าสิ่งนี้: "รอหนึ่งรายการ (expr ที่รอคอย), จากนั้นดันผลลัพธ์ไปที่ temp จากนั้นใช้รายการถัดไป (การวนซ้ำถัดไปของ for loop)" กำลังรอ "สำหรับแต่ละรายการนั้นสมบูรณ์ จำกัด การวนซ้ำเดียวของลูปถ้าการทดสอบแสดงให้เห็นว่ามีการขนานมันจะต้องเป็นเพราะ transpiler กำลังทำอะไรบางอย่างที่ไม่เป็นมาตรฐานหรือถูกแบนรถ
Szczepan Hołyszewski

@ SzczepanHołyszewskiความมั่นใจของคุณเกี่ยวกับการไม่เชื่อฟังโดยไม่ต้องเรียกใช้กรณีทดสอบเป็นแรงบันดาลใจให้ฉันทำการเปลี่ยนชื่อ refactory และความคิดเห็นเพิ่มเติม รหัสทั้งหมดเป็น ES6 ธรรมดาธรรมดาไม่จำเป็นต้องมีการ transpiling
Fred Yang

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