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
เมื่อหลายวันก่อน เราไปทำงานแล้วใช้ Terminal แบบปีศาจมาก ๆ จนเพื่อนถามว่า เราทำยังไงถึงสามารถสลับ Terminal Session ไปมาได้แบบบ้าคลั่งขนาดนั้น เบื้องหลังของผมน่ะเหรอกัปตัน ผมใช้ tmux ยังไงละ วันนี้เราจะมาแชร์ให้อ่านกันว่า มันเอามาใช้งานจริงได้อย่างไร เป็น Beginner Guide สำหรับคนที่อยากลองละกัน...
Firewall ถือว่าเป็นเครื่องมือในการป้องกันภัยขั้นพื้นฐานที่ปัจจุบันใคร ๆ ก็ติดตั้งใช้งานกันอยู่แล้ว แต่หากเรากำลังใช้ Ubuntu อยู่ จริง ๆ แล้วเขามี Firewall มาให้เราใช้งานได้เลยนะ มันชื่อว่า UFW วันนี้เราจะมาทำความรู้จัก และทดลองตั้ง Rule สำหรับการดักจับการเชื่อมต่อที่ไม่เกี่ยวข้องกันดีกว่า...
Obsidian เป็นโปรแกรมสำหรับการจด Note ที่เรียกว่า สารพัดประโยชน์มาก ๆ เราสามารถเอามาทำอะไรได้เยอะมาก ๆ หนึ่งในสิ่งที่เราเอามาทำคือ นำมาใช้เป็นระบบสำหรับการจัดการ Todo List ในแต่ละวันของเรา ทำอะไรบ้าง วันนี้เราจะมาเล่าให้อ่านกันว่า เราจัดการะบบอย่างไร...
อะ อะจ๊ะเอ๋ตัวเอง เป็นยังไงบ้างละ เมื่อหลายเดือนก่อน เราไปเล่าเรื่องกันขำ ๆ ว่า ๆ จริง ๆ แล้วพวก Loop ที่เราใช้เขียนโปรแกรมกันอยู่ มันไม่มีอยู่จริง สิ่งที่เราใช้งานกันมันพยายาม Abstract บางอย่างออกไป วันนี้เราจะมาถอดการทำงานของ Loop จริง ๆ กันว่า มันทำงานอย่างไรกันแน่ ผ่านภาษา Assembly...