By Arnon Puitrakul - 24 พฤศจิกายน 2021
ปัจจุบันยอมรับว่า ลักษณะการเขียนโปรแกรมใหม่ ๆ ออกมามากขึ้นเรื่อย ๆ และการทำงานก็ซับซ้อนมากขึ้นเรื่อย ๆ การทำงานแบบเก่า ๆ อย่างพวก Thread-Blocking อาจจะไม่ใช้คำตอบของโปรแกรมในสมัยใหม่ ๆ เท่าไหร่ เพราะเรามีการทำงานที่หลากหลายมากขึ้น เราต้องการให้ CPU ทำงานได้มีประสิทธิภาพสูงสุด ทำให้การเขียนโปรแกรมแบบ Asynchronous เข้ามามีบทบาทมากขึ้น ในวันนี้เราจะพาไปรู้จัก Foundation Concept ที่เราใช้ในการเขียนโปรแกรมแบบ Asynchronous อย่าง Coroutine กันว่า ในภาษา Python เราจะ Implement มันได้อย่างไร
def loop_me () :
for i in range(10) :
print(i)
โดยทั่ว ๆ ไปแล้ว เวลาเราสร้าง Function บน Python เราก็จะทำแบบด้านบน หรือเราอาจจะมีค่าอะไรบางอย่าง Return กลับไปให้ Caller หรือผู้เรียก การทำงานของมันก็จะเริ่มทำงานจากบนลงล่าง ไม่ได้มีความสามารถในการหยุด และ กลับมาทำงานต่อได้เลย พวกนี้คือ Function ที่เราใช้งานกันทั่ว ๆ ไปใน Python
ก่อนที่เราจะไปไหนไกล เราเริ่มจากทำความรู้จักกับคำว่า Coroutine กันก่อน ถ้าเราลองดูดี ๆ มันจะแยกออกมาเป็น 2 คำคือ คำว่า Co- ที่เป็น Prefix แปลว่า ร่วมกัน ด้วยกัน และ Routine ที่แปลว่ากิจวัตร ทำให้เมื่อมันอยู่รวมกัน มันก็น่าจะแปลประมาณว่า การทำงานร่วมกันของ Routine ที่มากกว่า 1 ตัวขึ้นไป
อ่านแล้วอาจจะ งง ว่า Routine อะไร ถ้าเรามองในแง่ของโปรแกรมมันก็คือ ส่วนหนึ่งของโปรแกรม ทำให้ในคอมพิวเตอร์ Coroutine มันก็จะพูดถึงการทำงานหลาย ๆ ส่วนของโปรแกรมนั่นเอง โดยปกติแล้วบน CPU สมัยใหม่ เรามีหลาย Core ให้เราสามารถทำงานได้ พวกนั้นเราก็อาจจะใช้พวกการแบ่ง Process หรือการทำ Threading เพื่อให้หลาย ๆ ส่วนของ โปรแกรม หรือส่วนเดียวกันแต่แบ่ง Input สามารถทำงานพร้อม ๆ กันได้ เพื่อเพิ่ม Yield ในการประมวลผลให้สูงขึ้น
ปัญหาของการทำแบบนั้นคือ เมื่อมันต้องมีการเรียกของที่มันต้องรอนาน ๆ เช่น Disk I/O และ Network I/O หรือกระทั่งรอ อีก Thread ทำงานเสร็จมันก็จะต้องอาศัยเวลา ทำให้งานบางงาน เราแบ่ง Thread เยอะ ๆ ไป มันก็ไม่ได้ช่วยอะไรเลย ถามว่าแล้วทำยังไงละ
Concept ของ Coroutine เลยเข้ามาแก้ปัญหาว่า ไหน ๆ เราก็ต้องรอแล้ว เราก็หยุดการทำงานของส่วนนั้นออกไปก่อน แล้วให้อีกส่วนมันทำงานไป พอสิ่งที่ส่วนแรกต้องการมันมาถึงแล้ว เราก็ให้ส่วนแรกกลับมาทำงานต่ออีกครั้งไปเรื่อย ๆ นั่นเอง ทำให้เราสามารถมองภาพใหม่ได้ว่า มันก็คือ Function ที่มี Checkpoint เพื่อให้เราสามารถหยุด และ กลับมาทำงานต่อได้ก็ได้เหมือนกัน
def simple_coroutine () :
print("Part 1")
yield
print("Part 2")
func = simple_coroutine()
next(func)
next(func)
ใน Python เราสามารถเขียนเหมือนกับเป็น Checkpoint ใน Function ได้โดยการใช้คำสั่งที่ชื่อว่า Yield ถ้าเราคุ้นเคยกับ Generator อยู่แล้ว มันคือสิ่งเดียวกันเลย แต่แทนที่เราจะพ่นค่าอะไรสักอย่างออกมา เราก็ไม่ได้เอาค่าอะไรออกมาเลย เราแค่ให้มัน Yield ออกมาแค่นั้นเลย ส่วนด้านนอก เราก็สามารถสั่งให้ Function มันทำงานต่อได้โดยใช้คำสั่ง next
RESULT :
Part 1
Part 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
เมื่อเรารันออกมา เราก็จะเห็นว่า Part 1 และ 2 ทำงานออกมาได้อย่างที่เราเขียนเอาไว้ แต่เอ๊ะ มันมี Exception เด้งออกมา
def simple_coroutine () :
print("Part 1")
yield
print("Part 2")
try :
func = simple_coroutine()
next(func)
next(func)
except StopIteration as e:
pass
ใช่แล้วละ มันคือ StopIteration Exception ใน Python ซึ่งเราสามารถใช้ Try Except ในการดักตรงนี้ได้ หรือเอาให้เจ๋งกว่านั้นเอา เรายังสามารถที่จะโยนค่ากลับเข้าไปใน Function ได้ด้วย ผ่านคำสั่ง send
def sending_val_back () :
print("Part 1")
a = yield
print("Part", a)
try :
func = sending_val_back()
next(func)
func.send(3)
except StopIteration as e:
pass
RESULT:
Part 1
Part 3
จากตรงนี้ เราก็เขียน Function คล้าย ๆ เดิมเลย แต่ในตอน Part อันที่ 2 แทนที่เราจะบอกให้มันเขียน Part 2 ตรง ๆ เรากลับให้มันรับค่าเข้ามา ในที่นี้เราใส่ 3 เข้าไปผ่านคำสั่ง send ที่ด้านล่าง ทำให้ในรอบที่ 2 มันออกมาเป็น Part 3 จาก 3 ที่เราใส่เข้าไปนั่นเอง
แต่ถ้าเรามาอ่าน Code ดี ๆ แล้วเราลองอ่าน Code ดูดี ๆ เราจะเห็นจุดที่แปลก ๆ อยู่ที่นึงคือ ทำไมเราต้องเรียก next ก่อน ก่อนที่เราจะเรียก send ได้ ทั้ง ๆ ที่เราไม่ได้มี Checkpoint ก่อนหน้านิน่า นั่นเป็นเพราะ เราจะ send ได้ก็ต่อเมื่อเราอยู่ที่ Yield Checkpoint แล้วเท่านั้น ถ้าเราเรียก Send เลยมันจะพ่น Error ออกมาบอกว่ามันไม่สามารถส่งค่าที่ไม่ใช่ None ใน Generator ที่พึ่งเริ่มได้ ทำให้เราจะต้อง next มันทีนึงก่อนที่เราจะ send นั่นเอง
จาก Concept ของการ Pause/Resume Function ในหัวข้อก่อนหน้า เราก็สามารถเอาเรื่องตรงนี้แหละ มาใช้ในการทำ Coroutine ระหว่าง 2 Function ด้วยกัน
def function_1 () :
print("Function 1 Part 1")
yield
print("Function 1 Part 2")
yield
print("Function 1 Part 3")
def function_2 () :
print("Function 2 Part 1")
yield
print("Function 2 Part 2")
yield
print("Function 2 Part 3")
try :
func_1 = function_1()
func_2 = function_2()
next(func_1)
next(func_2)
next(func_1)
next(func_2)
next(func_1)
next(func_2)
except StopIteration as e:
pass
OUTPUT:
Function 1 Part 1
Function 2 Part 1
Function 1 Part 2
Function 2 Part 2
Function 1 Part 3
ในตัวอย่างนี้เราพยายามที่จะทำให้ 2 Function มีการทำงานร่วมกันใน Thread เดียวกัน แต่สลับกันทำงานไปมา Pattern ที่เราใช้ มันก็เหมือนกับก่อนหน้าที่เราทดลองกันมาหมดเลย แค่แทนที่เราจะใช้แค่ Function เดียว เราก็เอา 2 Function มาใส่ด้วยกัน
Coroutine เป็น Concept นึงที่เรียกได้ว่าเป็นเหมือนกับ อิฐ และ ปูนในการเขียนโปรแกรมแบบ Asynchronous เลยก็ว่าได้ มันทำให้เราสามารถรันหลาย ๆ Function หรือหลาย ๆ ส่วนของโปรแกรมใน Thread เดียวกัน ในส่วนของ Python เองเราก็สามารถ Implement Concept นี้ลงไปได้เช่นกันผ่านการใช้งานพวก Generator จากตัวอย่างที่เราลอง Implement Coroutine ให้ดูเป็นแค่พื้นฐานเท่านั้น แต่เราสามารถเอา Concept ตรงนี้แหละ ไปประกอบร่างรวมกับพวก Scheduler เพื่อทำให้มันทำงานในแบบที่เราต้องการได้เลย ตัวอย่างของหลาย ๆ Application ที่เอาเรื่องนี้ไปใช้พวก asyncio ที่เป็น Built-in Module ใน Python
เราเป็นคนที่อ่านกับซื้อหนังสือเยอะมาก ปัญหานึงที่ประสบมาหลายรอบและน่าหงุดหงิดมาก ๆ คือ ซื้อหนังสือซ้ำเจ้าค่ะ ทำให้เราจะต้องมีระบบง่าย ๆ สักตัวในการจัดการ วันนี้เลยจะมาเล่าวิธีการที่เราใช้ Obsidian ในการจัดการหนังสือที่เรามีกัน...
หากเราเรียนลงลึกไปในภาษาใหม่ ๆ อย่าง Python และ Java โดยเฉพาะในเรื่องของการจัดการ Memory ว่าเขาใช้ Garbage Collection นะ ว่าแต่มันทำงานยังไง วันนี้เราจะมาเล่าให้อ่านกันว่า จริง ๆ แล้วมันทำงานอย่างไร และมันมีเคสใดที่อาจจะหลุดจนเราต้องเข้ามาจัดการเองบ้าง...
ก่อนหน้านี้เราเปลี่ยนมาใช้ Zigbee Dongle กับ Home Assistant พบว่าเสถียรขึ้นเยอะมาก อุปกรณ์แทบไม่หลุดออกจากระบบเลย แต่การติดตั้งมันเข้ากับ Synology DSM นั้นมีรายละเอียดมากกว่าอันอื่นนิดหน่อย วันนี้เราจะมาเล่าวิธีการเพื่อใครเอาไปทำกัน...
เมื่อหลายวันก่อนมีพี่ที่รู้จักกันมาถามว่า เราจะโหลด CSV ยังไงให้เร็วที่สุด เป็นคำถามที่ดูเหมือนง่ายนะ แต่พอมานั่งคิด ๆ ต่อ เห้ย มันมีอะไรสนุก ๆ ในนั้นเยอะเลยนี่หว่า วันนี้เราจะมาเล่าให้อ่านกันว่า มันมีวิธีการอย่างไรบ้าง และวิธีไหนเร็วที่สุด เหมาะกับงานแบบไหน...