φυβλαςのβλογ
phyblasのブログ



การใช้ asyncio ใน python เพื่อทำให้โปรแกรมมีการถ่ายโอนแบบไม่ประสานเวลา
เขียนเมื่อ 2020/03/15 23:03
แก้ไขล่าสุด 2024/01/04 11:55





บทความนี้จะเขียนถึงการใช้มอดูล asyncio ในไพธอน เพื่อที่จะให้โปรแกรมทำงานโดยมีการถ่ายโอนข้อมูลแบบไม่ประสานเวลา (asynchronous IO)

วิธีนี้มีประโยชน์มากโดยเฉพาะเวลาที่เขียนโปรแกรมเกี่ยวกับการรับส่งข้อมูลทางอินเทอร์เน็ต




แนวคิดเรื่องการถ่ายโอนข้อมูลแบบไม่ประสานเวลา

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

ที่จริงแล้วในระหว่างที่รอข้อมูลส่งมาอยู่นั้น CPU ก็จะไม่ได้ทำการคำนวณอะไรและอาจจะว่างงาน ช่วงเวลาที่รออยู่นั้นถ้าสามารถใช้ทรัพยากรตรงนั้นไปทำอย่างอื่นได้ในขณะเดียวกันก็จะคุ้มค่ามากกว่าที่จะปล่อยให้ CPU อยู่เฉยๆระหว่างรอข้อมูล

ดังนั้นแล้วการให้ทำงานหลายอย่างไปพร้อมกันในเวลาเดียวกันจึงเป็นความคิดที่ดีเมื่อทำงานข้อมูลทางอินเทอร์เน็ต

วิธีหนึ่งที่ช่วยประหยัดเวลาในการรันได้ก็คือใช้มัลติโพรเซสซิง (multiprocessing) ดังที่ได้เขียนถึงไปใน https://phyblas.hinaboshi.com/20180317

นั่นคือใช้หลายตัวประมวลผลมารันพร้อมๆกัน ช่วยกันทำงาน จึงทำให้เร็วกว่าใช้ตัวเดียวตามปกติ

ส่วนอีกวิธีซึ่งจะแนะนำต่อไปนี้ก็คือ "อะซิงโครนัสไอโอ" (asynchronous IO) หรือแปลว่า "การถ่ายโอนข้อมูลแบบไม่ประสานเวลา"

โดย IO ในที่นี้หมายถึง input & output ก็คือการป้อนข้อมูลเข้าและส่งข้อมูลออกนั่นเอง คนละเรื่องกับ IO ที่พวกลุงเผด็จการใช้จ้างให้มาโจมตีประชาชน

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

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

เพื่อให้เห็นภาพชัด ขอยกตัวอย่างโดยแสดงเป็นภาพ สมมุติว่ามีงาน ๓ งาน ซึ่งถ้าทำไปทีละอย่างจะต้องใช้เวลาไปตามขั้นตอนดังนี้



ในที่นี้สมมุติว่าสีแดงแทนขั้นตอนส่วนที่ใช้ CPU ส่วนสีม่วงให้แทนขั้นตอนส่วนขนถ่ายข้อมูล

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



ถ้าหากเป็นอะซิงโครนัสจะมีการใช้ตัวประมาณผลแค่ตัวเดียว ดังนั้นส่วนที่ใช้ CPU (สีแดง) จะซ้อนทับกันไม่ได้ ตรงขั้นตอนการรอรับส่งข้อมูล (สีม่วง) จะซ้อนกันก็ไม่เป็นอะไร ดังนั้นสามารถจัดสรรให้สลับการทำงานไปทำแต่ละงานโดยพยายามให้ CPU ได้ใช้งานตลอดก็จะเป็นแบบนี้



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

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

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

เช่นเวลาที่ต้องการดึงข้อมูลจากเว็บอย่างรวดเร็วนั้นอะซิงโครนัสจะเป็นวิธีที่มีประสิทธิภาพยิ่งกว่ามัลติโพรเซสซิง




เกี่ยวกับมอดูล asyncio

วิธีการที่จะทำอะซิงโครนัสในไพธอนคือใช้มอดูล asyncio ซึ่งเป็นมอดูลติดตัวที่มีมากับตัวไพธอนโดยไม่ต้องติดตั้งเพิ่ม โดยเพิ่งเริ่มมีในไพธอน 3.4 เป็นต้นมา

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

เช่นในไพธอน 3.5 ได้เพิ่มไวยากรณ์ async กับ await ขึ้นมา ส่วนไพธอน 3.7 ก็เพิ่มฟังก์ชันที่สะดวกเช่นพวก asyncio.run ขึ้นมา

ฟังก์ชันที่ถูกเพิ่มเข้ามาในไพธอน 3.7 นั้นทำให้การใช้ asyncio ทำได้ง่ายขึ้นมาก ลดความซับซ้อนลง เขียนโค้ดสั้นลง เพราะเวอร์ชันก่อนหน้านั้นจะต้องยุ่งกับสิ่งที่เรียกว่า "อีเวนต์ลูป" (event loop) โดยตรง แต่ในไพธอน 3.7 มีฟังก์ชันที่ทำให้โดยอัตโนมัติ

เพียงแต่ว่าในการใช้งานในระดับสูงขึ้นไปนั้นความเข้าใจเรื่องอีเวนต์ลูปยังจำเป็นอยู่

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

และตัวอย่างต่อไปนี้หากรันใน IDE บางตัวเช่น spyder อาจจะไม่ได้ผลอย่างที่ต้องการ เกี่ยวกับเรื่องนี้เขียนถึงไว้ใน https://phyblas.hinaboshi.com/20200311

การใช้มอดูล nest_asyncio จะช่วยให้รันใน spyder ได้ แต่ก็ไม่แนะนำนัก ทางที่ดีคือรันจากคอมมานด์ไลน์โดยตรงดีกว่า




การสร้างและใช้งานโครูทีน

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

คำสั่ง async มีไว้สำหรับสร้างสิ่งที่เรียกว่า "โครูทีน" (coroutine) ขึ้นมา โดยการสร้างโครูทีนนั้นจะคล้ายกับการสร้างฟังก์ชันธรรมดา นั่นคือใช้คำสั่ง def แต่จะนำหน้าด้วย async

ฟังก์ชันที่สร้างขึ้นด้วย async def จะเรียกว่าเป็น "ฟังก์ชันโครูทีน" (coroutine function) คือเป็นฟังก์ชันที่สร้างโครูทีน แทนที่จะทำงานเหมือนฟังก์ชันทั่วไปที่สร้างด้วย def เฉยๆ

ตัวอย่างการสร้างฟังก์ชันโครูทีน
import asyncio

async def buaklek(a,b):
    return a+b

print(buaklek) # ได้ <function buaklek at 0x11c7a8710>
print(type(buaklek)) # ได้ <class 'function'>
coru = buaklek(5,4)
print(coru) # ได้ <coroutine object buaklek at 0x11c7a87a0>
print(type(coru)) # ได้ <class 'coroutine'>

phonbuak = asyncio.run(coru) # ทำการรันโครูทีนเพื่อให้ฟังก์ชันทำงาน
print('ผลบวก: %s'%phonbuak) # ได้ ผลบวก: 9

เมื่อมี async นำหน้า def ฟังก์ชันนี้จะไม่ใช่ฟังก์ที่เรียกแล้วใช้งานได้ทันทีตามปกติ แต่เป็นฟังก์ชันที่เรียกแล้วจะเป็นการสร้างโครูทีนขึ้นมา

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

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

จากนั้นต้องนำโครูทีนนั้นมารันอีกทีจึงจะเกิดการทำงานในฟังก์ชันขึ้น วิธีการรันโครูทีนมีอยู่หลายวิธี แต่วิธีที่ง่ายที่สุดก็คือใช้ฟังก์ชัน asyncio.run() ดังที่ใช้ในตัวอย่างนี้

asyncio.run() เป็นฟังก์ชันที่เพิ่งมีใช้ในไพธอน 3.7 ซึ่งทำให้เราไม่ต้องเล่นกับอีเวนต์ลูปโดยตรงเหมือนเวอร์ชันก่อน ส่วนวิธีการรันอีเวนต์ลูปโดยตรงจะกล่าวถึงเล็กน้อยตอนท้าย

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

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

แต่โครูทีนด้านในแค่ถูกสร้างขึ้นมาเฉยๆจะยังไม่ทำงาน แต่จะทำงานได้เมื่อมีการเปลี่ยนมันเป็นสิ่งที่เรียกว่า "ภารกิจ" (task)

ตรงนี้มีตัวละครใหม่ที่จำเป็นต้องทำความเข้าใจให้ดีโผล่ขึ้นมาอีก นั่นคือ task ถ้าแปลเป็นไทยก็คือ "ภารกิจ"

วิธีสร้างภารกิจขึ้นมาจากโครูทีนก็คือใช้ฟังก์ชัน asyncio.create_task()

ขอยกตัวอย่างดังนี้
import asyncio
    
async def buaklek(a,b):
    c = a+b
    print('ฟังก์ชัน buaklek เริ่มทำงาน a+b=%s'%c)
    return c

async def main():
    coru = buaklek(13,10)
    task = asyncio.create_task(coru)
    print(task)
    print(type(task))
    print('สิ้นสุด main')

maincoru = main()
asyncio.run(maincoru)

จะได้ผลออกมาแบบนี้
<Task pending coro=<buaklek() running at /home/phyblas/asyn.py:3>>
<class 'asyncio.tasks.Task'>
สิ้นสุด main
ฟังก์ชัน buaklek เริ่มทำงาน a+b=23

ในตัวอย่างนี้ได้สร้างฟังก์ชัน ๒ อัน โดยใช้ async ทั้งคู่ ฟังก์ชัน main มีการสร้างโครูทีนของฟังก์ชัน buaklek ขึ้น จากนั้นเอาโครูทีนมาทำเป็นภารกิจ

เมื่อใช้ asyncio.create_task() เราจะได้ออบเจ็กต์ของภารกิจ คือคลาส asyncio.tasks.Task

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

ในฟังก์ชัน buaklek ในที่นี้ได้มีการสั่ง print ด้วย ดังนั้นเมื่อดูจากผลลัพธ์เราจึงรู้ได้ว่าฟังก์ชันนี้ทำงานเมื่อไหร่ จะเห็นว่า print ในฟังก์ชันทำงานทีหลัง type(task) นั่นคือมันทำงานตอนที่ฟังก์ชัน main สิ้นสุดลง

แต่ว่าผลลัพธ์ที่ได้จากฟังก์ชัน buaklek() จะไปอยู่ตรงไหน ในตัวอย่างนี้จะเห็นว่าเรารู้ว่าฟังก์ชันมีการทำงานจริงๆเพราะมีการ print แต่คำตอบ return c ไม่ได้ถูกนำไปใช้ เพราะฟังก์ชันนี้ถูกเรียกโดยอัตโนมัติหลังสิ้นสุดฟังก์ชัน main และไม่ได้ถูกเก็บในตัวแปรไหน

เพื่อที่จะเรียกให้ฟังก์ชันในภารกิจทำงานและคืนค่าออกมาทันที จะใช้คำสั่ง await แบบนี้
async def buaklek(a,b):
    c = a+b
    print('==ฟังก์ชันเริ่มทำงาน==')
    return c

async def main():
    coru = buaklek(13,10)
    task = asyncio.create_task(coru)
    phonbuak = await task
    print('ได้ผลบวก %s'%phonbuak)

maincoru = main()
asyncio.run(maincoru)

ได้
==ฟังก์ชันเริ่มทำงาน==
ได้ผลบวก 23

ครั้งนี้ฟังก์ชันของภารกิจ task ถูกสั่งให้ทำงานโดยตรงด้วยคำสั่ง await จึงทำงานในจังหวะนั้นตรงนั้นเลย และจะให้ค่าคำตอบออกมา

await ในที่นี้เป็นรูปแบบไวยากรณ์ใหม่ เช่นเดียวกับ async ไม่ใช่ฟังก์ชัน จึงไม่ต้องใส่วงเล็บ () และมีวิธีใช้ที่ตายตัว คือเขียน await ตามด้วยออบเจ็กต์ภารกิจที่ต้องการให้ทำงาน
await ภารกิจ

ถ้าต้องการรับค่าที่คืนออกมาด้วยก็เขียน = แบบนี้
ตัวแปรที่รับค่าที่คืนจากฟังก์ชัน = await ภารกิจ

จากตรงนี้ขอให้ข้อสรุปดังนี้:
- ภารกิจจะไม่ทำงานทันทีหลังจากที่ถูกสร้างขึ้นมา
- ภารกิจอาจเริ่มทำงานได้ด้วยสาเหตุต่างๆหลายอย่าง
- await เป็นวิธีหนึ่งที่ทำให้ฟังก์ชันในภารกิจเริ่มทำงาน
- หากภารกิจไม่ถูกสั่งให้ทำงานโดยตรง หรือมีสาเหตุอื่นให้เริ่มทำงาน มันก็จะถูกเรียกทำงานตอนจบฟังก์ชัน

await นอกจากจะใช้กับภารกิจแล้วยังใช้กับโครูทีนโดยตรงได้เลย ในกรณีนี้ภารกิจจะถูกสร้างขึ้นจากโครูทีนโดยอัตโนมัติแล้วก็ทำงานทันที

ดังนั้นสามารถเขียนแบบนี้ได้
async def buaklek(a,b):
    return a+b

async def main():
    phonbuak = await buaklek(13,10)
    print('ได้ผลบวก %s'%phonbuak)

asyncio.run(main())

ในที่นี้ buaklek(13,10) ได้เป็นโครูทีน ไม่ใช่ภารกิจ แต่ก็ส่งให้ await ได้เลย

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

ดังนั้นจะใช้โครูทีนโดยตรงเลย หรือสร้างเป็นภารกิจก่อนก็ได้

สิ่งที่อยู่หลัง await คือตัวโครูทีนหรือภารกิจที่สร้างจากโครูทีนเท่านั้น และค่าที่คืนกลับจากฟังก์ชันโครูทีนจะได้มาเมื่อใช้ await




เกี่ยวกับตัวออบเจ็กต์ภารกิจ asyncio.tasks.Task

ก่อนที่จะไปต่อไกลกว่านั้นจำเป็นจะต้องเข้าใจเกี่ยวกับออบเจ็กต์ของตัวภารกิจ คือออบเจ็กต์ asyncio.tasks.Task ซึ่งได้มาจากการใช้ asyncio.create_task

ภารกิจสามารถถูกสร้างขึ้นโดยตรงได้จากการสร้างอินสแตนซ์ของคลาส asyncio.tasks.Task โดยป้อนโครูทีนเข้าไปเช่น
import asyncio

async def khunlek(a,b):
    print('%s × %s = %s'%(a,b,a*b))

async def main():
    task = asyncio.tasks.Task(khunlek(4,28))
    print(task)

asyncio.run(main())

ได้
<Task pending coro=<khunlek() running at /home/phyblas/asyn.py:3>>
4 × 28 = 112
แต่ปกติจะไม่แนะนำให้ทำอย่างนั้น โดยทั่วไปจะแนะนำให้สร้างขึ้นด้วย asyncio.create_task() มากกว่า

ภารกิจที่ถูกสร้างออกมาแล้ว ถ้ายังไม่ได้ถูกทำเสร็จก็สามารถยกเลิกได้โดยใช้เมธอด .cancel() ภารกิจที่ยกเลิกไปแล้วนั้นหากสั่งให้ทำงานเช่นใช้ await ก็จะเกิดข้อผิดพลาด
async def khunlek(a,b):
    print('%s × %s = %s'%(a,b,a*b))

async def main():
    task = asyncio.create_task(khunlek(4,28))
    task.cancel()
    await task # ได้ CancelledError

asyncio.run(main())

ถ้าฟังก์ชันมีการ return คืนค่า นอกจากจะเอาค่าด้วย await แล้ว ยังสามารถเอาค่านั้นได้โดยใช้เมธอด .result()

ตัวอย่างเช่น ลองใช้ await ให้ภารกิจทำงานไปก่อน แล้วค่อยมาเอาค่าที่หลังด้วย .result() แบบนี้
async def khunlek(a,b):
    return a*b

async def main():
    task = asyncio.create_task(khunlek(17,4))
    await task
    print(task.result()) # ได้ 68

asyncio.run(main())

แต่ถ้าภารกิจนั้นยังไม่ได้ทำงานจนจบก็จะไม่มีผลลัพธ์ออกมา เมื่อใช้ .result() ก็จะเกิดข้อผิดพลาด เช่น
async def main():
    task = asyncio.create_task(khunlek(55,55))
    print(task.result()) # InvalidStateError: Result is not ready.

ถ้าจะตรวจสอบว่าภารกิจนั้นทำไปเสร็จหรือยังก็ทำได้โดยใช้เมธอด .done() เช่นทำแบบนี้ก็จะป้องกันการเกิดข้อผิดพลาดได้
async def khunlek(a,b):
  return a*b

async def main():
  task = asyncio.create_task(khunlek(3.5,2))
  if(task.done()):    
      print(task.result())
  else:
      print('ยังไม่เสร็จเลย')

asyncio.run(main())

ในไพธอน 3.8 มีเมธอดที่ใช้ได้เพิ่มขึ้นมาได้ในออบเจ็กต์ภารกิจ ๓ ตัว คือ
- .get_coro() เอาตัวออบเจ็กต์โครูทีนของภารกิจนี้
- .set_name() ตั้งชื่อให้ภารกิจ
- .get_name() ดูชื่อภารกิจ ถ้ายังไม่ได้ตั้งชื่อจะได้เป็นชื่อทีมีอยู่แล้ว

ตัวอย่าง
async def thamkapkhao():
    return 'กับข้าว'

async def main():
    task = asyncio.create_task(thamkapkhao())
    print('โครูทีน: %s'%task.get_coro())
    print('ชื่อภารกิจ: '+task.get_name())
    print('---ตั้งชื่อ---')
    task.set_name('ทำกับข้าว')
    print('ชื่อภารกิจ: '+task.get_name())

asyncio.run(main())

ได้
โครูทีน: 
ชื่อภารกิจ: Task-2
---ตั้งชื่อ---
ชื่อภารกิจ: ทำกับข้าว




การใช้ asyncio.sleep เพื่อแสดงถึงการใช้เวลารอ

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

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

ในที่นี้เราแค่ต้องการเรียนรู้หลักการเบื้องต้นของอะซิงโครนัสก่อนเฉยๆ ดังนั้นขอใช้วิธีตัวแทนที่เข้าใจง่าย นั่นคือใช้ asyncio.sleep()

เวลาสอนการใช้ asyncio เบื้องต้นนั้น asyncio.sleep() เป็นฟังก์ชันที่มักถูกนำมาใช้เป็นตัวอย่างสมมุติเพื่อแทนการทำงานจริงๆ

ฟังก์ชันนี้จะทำการหยุดรอตามเวลาที่เรากำหนดเป็นวินาที

ตัวอย่างการใช้
import asyncio,time

async def ioioio(wela,chue_ngan):
    print('เริ่ม%s เวลาผ่านไปแล้ว %.5f วินาที'%(chue_ngan,time.time()-t0))
    await asyncio.sleep(wela)
    print('%sเสร็จแล้ว เวลาผ่านไปแล้ว %.5f วินาที'%(chue_ngan,time.time()-t0))
    return

async def main():
    print('เริ่มต้นฟังก์ชัน')
    phara1 = asyncio.create_task(ioioio(1.5,'โหลดเพลง'))
    phara2 = asyncio.create_task(ioioio(2.5,'โหลดอนิเมะ'))
    phara3 = asyncio.create_task(ioioio(0.5,'โหลดหนัง'))
    print('สร้างภารกิจเสร็จแล้ว')
    await phara2
    print('สิ้นสุดฟังก์ชัน')

t0 = time.time()
asyncio.run(main())

ได้
เริ่มต้นฟังก์ชัน
สร้างภารกิจเสร็จแล้ว
เริ่มโหลดเพลง เวลาผ่านไปแล้ว 0.00038 วินาที
เริ่มโหลดอนิเมะ เวลาผ่านไปแล้ว 0.00043 วินาที
เริ่มโหลดหนัง เวลาผ่านไปแล้ว 0.00046 วินาที
โหลดหนังเสร็จแล้ว เวลาผ่านไปแล้ว 0.50719 วินาที
โหลดเพลงเสร็จแล้ว เวลาผ่านไปแล้ว 1.50825 วินาที
โหลดอนิเมะเสร็จแล้ว เวลาผ่านไปแล้ว 2.50929 วินาที
สิ้นสุดฟังก์ชัน

ในที่นี้เราสมมุติให้ ioioio เป็นฟังก์ชันสำหรับทำการโหลดข้อมูลอะไรบางอย่าง แต่เราไม่ได้โหลดจริงๆ แต่ใช้ asyncio.sleep() แทน แล้วทึกทักเอาว่าเราใช้เวลาตรงนี้ไปในการโหลดอะไรบางอย่างอยู่ พอโหลดเสร็จก็ print แสดงข้อความแจ้งออกมา บอกเวลาที่ใช้ไป

ในที่นี้เราได้ใช้ asyncio.create_task() เพื่อสร้างภารกิจออกมา ๓ ตัว จากนั้นเมื่อมีการใช้คำสั่ง await ให้กับตัวใดตัวหนึ่ง ภารกิจทั้ง ๓ ก็จะเร่ิมขึ้นทันที

โดยตัวที่ถูกสั่ง await (ในที่นี้คือ phara2 โหลดอนิเมะ) ไม่จำเป็นต้องถูกเริ่มก่อน แต่ลำดับจะเรียงตามลำดับที่สร้างภารกิจขึ้น จึงเริ่มจาก phara1 (โหลดเพลง) ก่อน แต่ phara2 และ phara3 ก็เริ่มตามในแทบจะทันที

จากนั้นก็จะมีการรอตามเวลาที่กำหนดไว้ ซึ่งโหลดหนังเสร็จเร็วสุดก็เสร็จก่อน ตามด้วยโหลดเพลง แล้วก็โหลดอนิเมะ

เมื่อคำสั่งที่ถูกสั่ง await นั่นคือโหลดอนิเมะเสร็จ ฟังก์ชันก็จะสิ้นสุดลง

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

นั่นหมายความว่าถ้าเปลี่ยนใน main() เป็นให้รอ phara3 ซึ่งมีระยะเวลารอแค่ 0.5 วินาทีแทน ผลของตัวที่เหลือก็จะไม่ออกมา
# ขอเขียนใหม่แค่ส่วน main()
async def main():
    print('เริ่มต้นฟังก์ชัน')
    phara1 = asyncio.create_task(ioioio(1.5,'โหลดเพลง'))
    phara2 = asyncio.create_task(ioioio(2.5,'โหลดอนิเมะ'))
    phara3 = asyncio.create_task(ioioio(0.5,'โหลดหนัง'))
    print('สร้างภารกิจเสร็จแล้ว')
    await phara3
    print('สิ้นสุดฟังก์ชัน')

ได้
เริ่มต้นฟังก์ชัน
สร้างภารกิจเสร็จแล้ว
เริ่มโหลดเพลง เวลาผ่านไปแล้ว 0.00037 วินาที
เริ่มโหลดอนิเมะ เวลาผ่านไปแล้ว 0.00112 วินาที
เริ่มโหลดหนัง เวลาผ่านไปแล้ว 0.00117 วินาที
โหลดหนังเสร็จแล้ว เวลาผ่านไปแล้ว 0.50463 วินาที
สิ้นสุดฟังก์ชัน

ในการใช้งานจริงเราคงไม่อาจรู้ได้แน่ชัดว่างานไหนจะเสร็จก่อน ดังนั้นวิธีเขียนแบบนี้จึงไม่เหมาะจะใช้จริง

ในมอดูล asyncio เองก็มีฟังก์ชันหลายตัวที่เหมาะจะใช้งานได้ดีเมื่อต้องการสั่งหลายภารกิจพร้อมกัน เช่น asyncio.wait(), asyncio.gather(), asyncio.as_completed()

อนึ่ง ในมอดูล time ก็มีฟังก์ชันที่คล้ายๆกันคือ time.sleep() แต่ใช้แทนกันไม่ได้ จะแทนที่ asyncio.sleep() ด้วย time.sleep() ไม่ได้




การสั่งพร้อมกันหลายงานโดยใช้ asyncio.wait

กรณีที่ต้องการให้โครูทีนหลายตัวทำงานไปพร้อมๆกันโดยมีการรอจนเสร็จทั้งหมด แบบนั้นอาจใช้ฟังก์ชัน asyncio.wait()

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

ตัวอย่าง
async def ioioio(wela,chue_ngan):
    print('เริ่มต้น%s ผ่านไปแล้ว %.6f วินาที'%(chue_ngan,time.time()-t0))
    await asyncio.sleep(wela)
    print('%sเสร็จสิ้น ผ่านไปแล้ว %.6f วินาที'%(chue_ngan,time.time()-t0))

async def main():
    cococoru = [ioioio(1.5,'โหลดเพลง'),ioioio(2.5,'โหลดอนิเมะ'),ioioio(0.5,'โหลดหนัง'),ioioio(2,'โหลดเกม')]
    await asyncio.wait(cococoru)

t0 = time.time()
asyncio.run(main())

ได้
เริ่มต้นโหลดหนัง ผ่านไปแล้ว 0.000816 วินาที
เริ่มต้นโหลดเกม ผ่านไปแล้ว 0.000929 วินาที
เริ่มต้นโหลดเพลง ผ่านไปแล้ว 0.000962 วินาที
เริ่มต้นโหลดอนิเมะ ผ่านไปแล้ว 0.000984 วินาที
โหลดหนังเสร็จสิ้น ผ่านไปแล้ว 0.503434 วินาที
โหลดเพลงเสร็จสิ้น ผ่านไปแล้ว 1.501357 วินาที
โหลดเกมเสร็จสิ้น ผ่านไปแล้ว 2.004098 วินาที
โหลดอนิเมะเสร็จสิ้น ผ่านไปแล้ว 2.505103 วินาที

ลำดับที่ที่ใส่ลงไปในลิสต์นั้นไม่มีผลต่อการเริ่มทำก่อนหรือหลัง




- ค่าคืนกลับและการกำหนดเวลาจำกัดเมื่อใช้ asyncio.wait

เมื่อใช้asyncio.wait สามารถกำหนดเวลาจำกัดได้โดยใส่คีย์เวิร์ด timeout เช่นถ้าใส่ timeout=1.8 แบบนี้
async def main():
    cococoru = [ioioio(1.5,'โหลดเพลง'),ioioio(2.5,'โหลดอนิเมะ'),ioioio(0.5,'โหลดหนัง'),ioioio(2,'โหลดเกม')]
    await asyncio.wait(cococoru,timeout=1.8)

ก็จะโหลดเสร็จแค่หนังกับเพลง ที่เหลือใช้เวลามากกว่านั้นจะไม่เสร็จ

ค่าคืนกลับที่ได้จาก asyncio.wait จะมี ๒ ส่วน คือเซ็ตของภารกิจที่รันเสร็จกับที่ยังไม่เสร็จ สามารถนำมาใช้ต่อเพื่อเอาผลลัพธ์จากฟังก์ชันได้

ลองเขียนใหม่ให้ฟังก์ชันมีการคืนค่ามาด้วย แล้วเราก็ใช้ asyncio.wait แล้วรับเซ็ตของภารกิจที่ได้ออกมาแล้วใช้ดู
async def ioioio(wela,chue_ngan):
    await asyncio.sleep(wela)
    print('โหลด%sเสร็จ ผ่านไปแล้ว %.6f วินาที'%(chue_ngan,time.time()-t0))
    return chue_ngan

async def main():
    cococoru = [ioioio(1.5,'เพลง'),ioioio(2.5,'อนิเมะ'),ioioio(0.5,'หนัง'),ioioio(2,'เกม')]
    setlaeo,yangmaiset = await asyncio.wait(cococoru,timeout=1.8)
    print('---')
    print('เสร็จแล้ว %d งาน ยังไม่เสร็จอีก %d งาน'%(len(setlaeo),len(yangmaiset)))
    print('---')
    for phara in setlaeo:
        khong = await phara
        print('~%s~ เสร็จไปแล้ว'%khong)
    print('---')
    for phara in yangmaiset:
        khong = await phara
        print('~%s~ เพิ่งเสร็จ'%khong)

t0 = time.time()
asyncio.run(main())

ได้
โหลดหนังเสร็จ ผ่านไปแล้ว 0.542386 วินาที
โหลดเพลงเสร็จ ผ่านไปแล้ว 1.542869 วินาที
---
เสร็จแล้ว 2 งาน ยังไม่เสร็จอีก 2 งาน
---
~หนัง~ เสร็จไปแล้ว
~เพลง~ เสร็จไปแล้ว
---
โหลดเกมเสร็จ ผ่านไปแล้ว 2.041564 วินาที
~เกม~ เพิ่งเสร็จ
โหลดอนิเมะเสร็จ ผ่านไปแล้ว 2.541081 วินาที
~อนิเมะ~ เพิ่งเสร็จ

ในที่นี้หนังกับเพลงโหลดเสร็จก่อน 1.8 วินาทีที่กำหนด ทำให้จะได้ภารกิจของมันมาอยู่ในเซ็ตตัวซ้าย คือ setlaeo

ส่วนเกมกับอนิเมะจะยังไม่เสร็จ จึงได้ภารกิจของเกมกับอนิเมะออกมาอยู่เซ็ตตัวขวา คือ yangmaiset

ในที่นี้นำเซ็ตที่ได้มาวนด้วย for เพื่อเอาภารกิจมาจัดการทำอะไรต่อทีละตัว

สำหรับ ๒ ภารกิจแรกคือหนังกับเพลงนั้น เนื่องจากเสร็จสิ้นไปตั้งแต่ตอนใช้ asyncio.wait แล้ว ดังนั้นเมื่อเจอ await อีกก็ให้ผลออกมาทันที

และเนื่องจากงานเสร็จไปแล้วและมีผลลัพธ์เตรียมไว้แล้ว จึงอาจใช้เมธอด .result() แทน await ได้ คือเขียนเป็น khong = phara.result()

ส่วน ๒ ภารกิจหลังคือเกมกับอนิเมะซึ่งอยู่ใน yangmaiset นั้น เนื่องจากยังไม่ได้ถูกทำจนเสร็จ เมื่อเจอ await ภายใน for จึงค่อยเริ่มมาทำต่อจนเสร็จในตอนนั้น

และเนื่องจากเป็นภารกิจที่ยังไม่เสร็จ ดังนั้นจะใช้เมธอด .result() แทน await ไม่ได้ ถ้าใช้จะเกิดข้อผิดพลาดขึ้น




- การกำหนดเงื่อนไขการสิ้นสุด asyncio.wait

ปกติ asyncio.wait จะรอจนทุกภารกิจเสร็จหมด แต่สามารถใส่ค่าคีย์เวิร์ด return_when เพื่อเปลี่ยนเงื่อนไขได้

ถ้าใส่เป็น FIRST_COMPLETED จะสิ้นสุดเมื่อมีงานตัวใดตัวหนึ่งทำเสร็จไป
item = []
async def ioioio(wela,chue_ngan):
    await asyncio.sleep(wela)
    item.append('@'+chue_ngan+'@')

async def main():
    cococoru  = [ioioio(1.5,'เพลง'),ioioio(2.5,'อนิเมะ'),ioioio(0.5,'หนัง'),ioioio(2,'เกม')]
    setlaeo,yangmaiset = await asyncio.wait(cococoru,return_when='FIRST_COMPLETED')
    print('เสร็จแล้ว %s งาน ยังไม่เสร็จ %s งาน'%(len(setlaeo),len(yangmaiset)))
    print('สิ่งที่โหลดเสร็จแล้ว: %s'%item)
        
asyncio.run(main())

ได้
เสร็จแล้ว 1 งาน ยังไม่เสร็จ 3 งาน
สิ่งที่โหลดเสร็จแล้ว: ['@หนัง@']

แต่ถ้าไม่ได้ใส่ return_when ไป หรือใส่ return_when='ALL_COMPLETED' ไป ผลที่ได้ควรจะออกมาเสร็จหมดทั้ง ๔ งาน
เสร็จแล้ว 4 งาน ยังไม่เสร็จ 0 งาน
สิ่งที่โหลดเสร็จแล้ว: ['@หนัง@', '@เพลง@', '@เกม@', '@อนิเมะ@']

ถ้าใส่เป็น FIRST_EXCEPTION จะสิ้นสุดลงเมื่อตัวใดตัวหนึ่งเกิดข้อผิดพลาดขึ้นในฟังก์ชัน

เช่น ลองสร้างฟังก์ชันให้เกิดข้อผิดพลาดขึ้นถ้าเวลาผ่านไปมากเกิน 1.2 วินาที
item = []
async def ioioio(wela,chue_ngan):
    await asyncio.sleep(wela)
    item.append('@'+chue_ngan+'@')
    if(time.time()-t0>1.2):
       raise Exception

async def main():
cococoru  = [ioioio(1.5,'เพลง'),ioioio(2.5,'อนิเมะ'),ioioio(0.5,'หนัง'),ioioio(2,'เกม')]
    setlaeo,yangmaiset = await asyncio.wait(cococoru,return_when='FIRST_EXCEPTION')
    print('เสร็จแล้ว %s งาน ยังไม่เสร็จ %s งาน'%(len(setlaeo),len(yangmaiset)))
    print('สิ่งที่โหลดเสร็จแล้ว: %s'%item)
        
t0 = time.time()
asyncio.run(main())

แบบนี้ก็จะมีงานแรกงานเดียวที่เสร็จทันเวลา จึงหยุดหลังจากที่ตัวที่ ๒ ทำเสร็จ
เสร็จแล้ว 2 งาน ยังไม่เสร็จ 2 งาน
สิ่งที่โหลดเสร็จแล้ว: ['@หนัง@', '@เพลง@']




การสั่งพร้อมกันหลายงานโดยใช้ asyncio.gather

นอกจาก asyncio.wait แล้ว ยังมีอีกฟังก์ชันหนึ่งที่ใช้งานได้คล้ายๆกัน คือ asyncio.gather

asyncio.gather นั้นใช้สั่งให้โครูทีนทำงานหลายๆงานไปในเวลาเดียวกันเช่นเดียวกับ asyncio.wait เพียงแต่ว่าข้อแตกต่างที่สำคัญคือมีลำดับแน่นอน

ตัวอย่างการใช้
async def ioioio(wela,chue_khanom):
    print('เริ่มอบ%s ผ่านไปแล้ว %.5f วินาที'%(chue_khanom,time.time()-t0))
    await asyncio.sleep(wela)
    print('อบ%sเสร็จ ผ่านไปแล้ว %.5f วินาที'%(chue_khanom,time.time()-t0))
    return '*'+chue_khanom+'อบเสร็จ*'

async def main():
    cococoru = [ioioio(2,'เต้าหู้'),ioioio(3.5,'เค้ก'),ioioio(3,'ไส้กรอก'),ioioio(1,'ครัวซอง')]
    phonlap = await asyncio.gather(*cococoru)
    print(phonlap)

t0 = time.time()
asyncio.run(main())

ได้
เริ่มอบเต้าหู้ ผ่านไปแล้ว 0.00096 วินาที
เริ่มอบเค้ก ผ่านไปแล้ว 0.00110 วินาที
เริ่มอบไส้กรอก ผ่านไปแล้ว 0.00114 วินาที
เริ่มอบครัวซอง ผ่านไปแล้ว 0.00116 วินาที
อบครัวซองเสร็จ ผ่านไปแล้ว 1.00498 วินาที
อบเต้าหู้เสร็จ ผ่านไปแล้ว 2.00614 วินาที
อบไส้กรอกเสร็จ ผ่านไปแล้ว 3.00318 วินาที
อบเค้กเสร็จ ผ่านไปแล้ว 3.50442 วินาที
['*เต้าหู้อบเสร็จ*', '*เค้กอบเสร็จ*', '*ไส้กรอกอบเสร็จ*', '*ครัวซองอบเสร็จ*']

งานทั้ง ๔ ที่สั่งให้ asyncio.gather ไปจะเริ่มทำงานตามลำดับ แต่จะดำเนินไปพร้อมๆกันและจะเสร็จเมื่อไหร่ก็ขึ้นอยู่กับเวลาที่ใช้ และสุดท้ายผลลัพธ์ที่ return คืนออกมาจะได้ออกมาเป็นลิสต์ ซึ่งจะเรียงตามลำดับตามที่ใส่เข้าไป

asyncio.gather จะให้ค่ากลับมาเป็นลิสต์ของคำตอบของฟังก์ชันทันที ซึ่งต่างจาก asyncio.wait ที่จะให้เซ็ตของภารกิจ ต้องมาเอาค่าจากในนั้นอีกที

อีกทั้งของ asyncio.gather จะให้เป็นลิสต์โดยเรียงตามลำดับที่เราใส่ไป ในขณะที่ asyncio.wait จะไม่มีลำดับเพราะเป็นเซ็ต ไม่ใช่ลิสต์

และ asyncio.gather ไม่สามารถกำหนดเวลาจำกัดหรือเปลี่ยนเงื่อนไขการสิ้นสุดการทำงานได้เหมือนอย่าง asyncio.wait

asyncio.gather เหมือนกับ asyncio.wait ตรงที่จะรับค่าเป็นภารกิจโดยตรงเลย หรือว่าจะรับมาเป็นโครูทีนก็ได้ หากรับเป็นโครูทีนก็จะถูกแปลงเป็นภารกิจโดยอัตโนมัติ แล้วจึงเริ่มทำงาน

ข้อควรระวังอีกอย่างหนึ่งคือ asyncio.gather จะไม่ได้รับข้อมูลเป็นลิสต์​เหมือนอย่าง asyncio.wait แต่จะรับอาร์กิวเมนต์เป็นตัวๆ หากเตรียมข้อมูลใส่ลิสต์ไว้ก็ต้องใช้ * นำหน้าเพื่อให้ลิสต์กระจายไปเป็นอาร์กิวเมนต์ทีละตัวด้วย

ปกติแล้วถ้าเกิดมีข้อผิดพลาดเกิดขึ้นขณะรันตัวใดตัวหนึ่งจะทำให้การทำงานหยุดลงทันที แต่หากใส่ return_exceptions=True เมื่อเกิดข้อผิดพลาดจะไม่หยุดทำงาน แต่จะคืนค่าความผิดพลาดนั้นมาแทน

ตัวอย่างเช่น ลองสร้างฟังก์ชันให้มีเวลาจำกัด ถ้านานเกินไปกำหนดเวลาจะเกิดข้อผิดพลาด
async def ioioio(wela,chue_khanom):
    print('เริ่มอบ%s ผ่านไปแล้ว %.5f วินาที'%(chue_khanom,time.time()-t0))
    await asyncio.sleep(wela)
    if(wela>5):
        raise asyncio.TimeoutError('%sอบนานไป'%chue_khanom)
    print('อบ%sเสร็จ ผ่านไปแล้ว %.5f วินาที'%(chue_khanom,time.time()-t0))
    return '*'+chue_khanom+'อบเสร็จ*'

async def main():
    cococoru = [ioioio(2,'ไส้อั่ว'),ioioio(8,'ข้าวโพด'),ioioio(10,'มันฝรั่ง'),ioioio(1.5,'คุกกี้')]
    phonlap = await asyncio.gather(*cococoru,return_exceptions=True)
    print(phonlap)

t0 = time.time()
asyncio.run(main())

ได้
เริ่มอบไส้อั่ว ผ่านไปแล้ว 0.00121 วินาที
เริ่มอบข้าวโพด ผ่านไปแล้ว 0.00205 วินาที
เริ่มอบมันฝรั่ง ผ่านไปแล้ว 0.00218 วินาที
เริ่มอบคุกกี้ ผ่านไปแล้ว 0.00221 วินาที
อบคุกกี้เสร็จ ผ่านไปแล้ว 1.50391 วินาที
อบไส้อั่วเสร็จ ผ่านไปแล้ว 2.00675 วินาที
['*ไส้อั่วอบเสร็จ*', TimeoutError('ข้าวโพดอบนานไป'), TimeoutError('มันฝรั่งอบนานไป'), '*คุกกี้อบเสร็จ*']

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




การสั่งพร้อมกันหลายงานโดยใช้ asyncio.as_completed

asyncio.as_completed ก็เป็นอีกคำสั่งที่ใช้สั่งให้โครูทีนหรือภารกิจเริ่มทำงานไปพร้อมๆกันโดยไม่สนลำดับ เช่นเดียวกับ asyncio.wait

ข้อแตกต่างคือ asyncio.as_completed จะใช้งานคู่กับวังวน for

ปกติเวลาที่ใช้ for จะมีการไล่ทำงานไปตามลำดับทีละรอบ แต่สำหรับ for ที่ใช้กับ asyncio.as_completed นั้นจะค่อนข้างพิเศษ คือแต่ละตัวจะเริ่มทำงานไปพร้อมๆกัน

ตัวอย่างการใช้
async def ioioio(wela,chue_khong):
    print('เริ่มสั่งซื้อ%s ผ่านไปแล้ว %.5f วินาที'%(chue_khong,time.time()-t0))
    await asyncio.sleep(wela)
    print('ซื้อ%sเสร็จ ผ่านไปแล้ว %.5f วินาที'%(chue_khong,time.time()-t0))
    return '#'+chue_khong+'#'

async def main():
    cococoru = [ioioio(2,'กระเป๋า'),ioioio(2.5,'กระจก'),ioioio(1.5,'แก้วน้ำ'),ioioio(0.5,'กังหัน')]
    for coru in asyncio.as_completed(cococoru):
        phonlap = await coru
        print('ได้รับ %s'%phonlap)

t0 = time.time()
asyncio.run(main())

ได้
เริ่มสั่งซื้อกระจก ผ่านไปแล้ว 0.00111 วินาที
เริ่มสั่งซื้อกังหัน ผ่านไปแล้ว 0.00117 วินาที
เริ่มสั่งซื้อกระเป๋า ผ่านไปแล้ว 0.00120 วินาที
เริ่มสั่งซื้อแก้วน้ำ ผ่านไปแล้ว 0.00122 วินาที
ซื้อกังหันเสร็จ ผ่านไปแล้ว 0.50651 วินาที
ได้รับ #กังหัน#
ซื้อแก้วน้ำเสร็จ ผ่านไปแล้ว 1.50319 วินาที
ได้รับ #แก้วน้ำ#
ซื้อกระเป๋าเสร็จ ผ่านไปแล้ว 2.00412 วินาที
ได้รับ #กระเป๋า#
ซื้อกระจกเสร็จ ผ่านไปแล้ว 2.50270 วินาที
ได้รับ #กระจก#

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

สามารถกำหนดเวลาจำกัดด้วยคีย์เวิร์ด timeout ถ้าถึงเวลาที่กำหนดจะเกิด TimeoutError ขึ้นทันที งานทั้งหมดทำไปถึงแค่ไหนแล้วก็จบไปแค่นั้น
async def ioioio(wela,chue_khong):
    await asyncio.sleep(wela)
    print('ซื้อ%sเสร็จแล้ว'%chue_khong)

async def main():
    cococoru = [ioioio(10,'ข้าวจี่'),ioioio(2.5,'ข้าวเกรียบ'),ioioio(5.5,'ข้าวเหนียว')]
    for coru in asyncio.as_completed(cococoru,timeout=3):
        await coru

asyncio.run(main()) # ได้ TimeoutError

แต่ข้อผิดพลาดที่เกิดขึ้นนี้สามารถระงับได้ด้วย try except ได้ เช่น
async def ioioio(wela,chue_khong):
    await asyncio.sleep(wela)
    print('ผ่านไปแล้ว %.5f วินาที'%(time.time()-t0))
    return '~%s~'%chue_khong

async def main():
    cococoru = [ioioio(6.5,'ปาปริก้า'),ioioio(1.5,'ทวิสตี้'),ioioio(4,'โดริโทส'),ioioio(2,'ฮานามิ')]
    asco = asyncio.as_completed(cococoru,timeout=2.5)
    for coru in asco:
        try:
            khong = await coru
            print('ได้ %s'%khong)
        except:
            print('--หมดเวลา--')

t0 = time.time()
asyncio.run(main())

ได้
ผ่านไปแล้ว 1.50231 วินาที
ได้ ~ทวิสตี้~
ผ่านไปแล้ว 2.00382 วินาที
ได้ ~ฮานามิ~
--หมดเวลา--
--หมดเวลา--




การจำกัดเวลาการทำงานด้วย asyncio.wait_for

ถ้ามีงานบางอย่างที่ต้องการจำกัดเวลาในการทำ อาจใช้ฟังก์ชัน asyncio.wait_for

ฟังก์ชันนี้จะสร้างภารกิจขึ้นจากโครูทีน คล้ายกับ asyncio.create_task แต่จะใส่อาร์กิวเมนต์ ๒ ตัว ตัวแรกคือโครูทีน (หรือเป็นภารกิจก็ได้) และตัวที่ ๒ คือ เวลาจำกัดเป็นวินาที ซึ่งถ้าเลยเวลาก็จะเกิดข้อผิดพลาดขึ้น

ตัวอย่าง
async def ioioio():
    await asyncio.sleep(2)

async def main():
    coru = ioioio()
    await asyncio.wait_for(coru,1)
    # เกิด TimeoutError
        
asyncio.run(main())

ข้อผิดพลาดที่เกิดขึ้นเป็นออบเจ็กต์ asyncio.TimeoutError ซึ่งเป็นตัวเตือนว่าเกิดการเกินเวลาที่กำหนด

อาจใช้คู่กับ try except แบบนี้เพื่อให้พอเกิดข้อผิดพลาดก็เปลี่ยนเป็นทำอย่างอื่นแทนที่จะสะดุดไปเลย
async def ioioio():
    await asyncio.sleep(3)
    return 'เสร็จสิ้น'

async def main():
    try:
        phon = await asyncio.wait_for(ioioio(),timeout=2)
        print(phon)
    except asyncio.TimeoutError:
        print('เกินเวลา')
    print('เวลาผ่านไป %.6f'%(time.time()-t0))

t0 = time.time()
asyncio.run(main())

ได้
เกินเวลา
เวลาผ่านไป 2.005006

แต่ถ้าไม่ได้ทำเกินเลยเวลาก็เสร็จตามเวลา ได้ผลตามปกติ ไม่เกิดข้อผิดพลาดอะไร แบบนี้
async def ioioio():
    await asyncio.sleep(1)

async def main():
    await asyncio.wait_for(ioioio(),2)
    print(time.time()-t0) # ได้ 1.0052869319915771

t0 = time.time()
asyncio.run(main())




ว่าด้วยเรื่องอีเวนต์ลูป

ดังที่ได้อธิบายไปว่าฟังก์ชัน asyncio.run เป็นคำสั่งที่มีตั้งแต่ไพธอน 3.7 เพื่อให้สามารถรันงานโครูทีนได้โดยที่ไม่ต้องไปเล่นกับอีเวนต์ลูป (event loop) โดยตรง

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

ตัวอย่างสั้นๆแสดงการใช้งานโครูทีนเมื่อเล่นกับอีเวนต์ลูปโดยตรง
import asyncio

async def hanlek(a,b):
    print('%s/%s'%(a,b))
    return a/b
        
loop = asyncio.get_event_loop() # สร้างอีเวนต์ลูป
phonhan = loop.run_until_complete(hanlek(7,6)) # เอาอีเวนต์ลูปมารัน
print('ผลลัพธ์: %.3f'%phonhan)

ได้
7/6
ผลลัพธ์: 1.167

ตรง ๒ บรรทัดที่ใช้ asyncio.get_event_loop() แล้วตามด้วย .run_until_complete นั้นถ้าหากใช้ asyncio.run ก็จะเขียนแค่
phonhan = asyncio.run(hanlek(7,6))

ในโปรแกรมที่ใช้อะซิงโครนัส อีเวนต์ลูปคือตัวกลางที่คอยจัดการโครูทีนและภารกิจทั้งหมดให้ โดยเมธอด asyncio.get_event_loop() จะสร้างอีเวนต์ลูปขึ้นมาใหม่ (แต่กรณีที่มีอีเวนต์ลูปอยู่แล้วจะใช้ที่มีอยู่) ส่วน loop.run_until_complete() เป็นคำสั่งให้เอาลูปนั้นมารันโครูทีนหรือภารกิจ

เวลาที่ใช้ asyncio.run นั้นจะมีการสร้างอีเวนต์ลูปขึ้นมาใหม่ให้แล้วก็ใช้รันทันที ทำให้เราไม่ต้องสร้างอีเวนต์ลูปโดยตรง

ในไพธอน 3.7 ขึ้นไปการที่ใช้ฟังก์ชัน asyncio.run ได้จึงทำให้เราอาจใช้อะซิงโครนัสได้โดยไม่จำเป็นต้องรู้เกี่ยวกับอีเวนต์ลูปมากนัก

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

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

สำหรับวิธีการใช้ aiohttp เขียนไว้ใน https://phyblas.hinaboshi.com/20200318

ที่สำคัญคือเข้าใจไวยากรณ์ใหม่ คือ async กับ await รู้ว่าเมื่อไหร่ต้องใส่ ถ้าใส่แล้วจะให้ผลเป็นยังไงบ้าง ตรงนี้เป็นสิ่งที่ต้องทวนและทำความเข้าใจให้ดี

ที่จริง async นั้นจริงๆแล้วไม่ได้ใช้แค่กับ def แต่ยังมีการใช้ async with หรือ async for ด้วย นี่ก็เป็นไวยากรณ์ที่ต้องมาทำความเข้าใจกันเพิ่มเติมอีก

เรื่องเกี่ยวกับ asyncio ยังมีรายละเอียดอีกมากมาย มีฟังก์ชันอื่นๆอีกมาก สำหรับบทความนี้ขอจบลงที่การใช้งานเบื้องต้นเท่านี้




อ้างอิง


-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python

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

目次

日本による名言集
モジュール
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
機械学習
-- ニューラル
     ネットワーク
javascript
モンゴル語
言語学
maya
確率論
日本での日記
中国での日記
-- 北京での日記
-- 香港での日記
-- 澳門での日記
台灣での日記
北欧での日記
他の国での日記
qiita
その他の記事

記事の類別



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  記事を検索

  おすすめの記事

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文