ปรับแต่ง Python Script ให้รันเร็วขึ้น 5 เท่าด้วย AsyncIO และ Multiprocessing
ผมเคยทำงานอยู่ที่บริษัทพัฒนาเกมมือถือชื่อ "Nova Games" ตอนปี 2026 เรากำลังพัฒนาเกมแนว RPG ใหม่ที่ต้องประมวลผลข้อมูลจำนวนมหาศาล เช่น สถานที่, ตัวละคร, ไอเทม, และการคำนวณสกิลเวลาผู้เล่นทำการโจมตี ปัญหาหลักคือ Script หลักที่เราเขียนด้วย Python ทำงานช้ามาก ทำให้ทีมต้องใช้เวลาในการทดสอบและปรับปรุงเกมนานเกินไป เราเสียเวลาไปกับการรอให้ Script ทำงานเสร็จ แถมยังทำให้เกิดปัญหาการทำงานขัดแย้งกันระหว่างส่วนต่างๆ ของ Script ด้วย ตอนนั้นเราต้องการหาทางแก้ไขอย่างเร่งด่วน เพื่อให้เกมสามารถปล่อยตัวได้ตามกำหนดที่วางไว้
ปัญหาพื้นฐานคือ Python เป็นภาษาแบบ Interpreter ซึ่งหมายความว่ามันต้องอ่านและตีความโค้ดทีละบรรทัด ทำให้การทำงานช้าลง ยิ่ง Script มีงานที่ต้องทำเยอะ เช่น การเรียก API จากภายนอก การประมวลผลข้อมูล หรือการส่งข้อมูลไปยังฐานข้อมูล มันก็จะยิ่งทำให้ Script รันช้าลงไปอีก การใช้เพียงแค่ Thread เดียวในการทำงานก็ไม่สามารถแก้ปัญหาได้ทั้งหมด เพราะ Python มี Global Interpreter Lock (GIL) ที่จำกัดการทำงานของ Thread หลายตัวพร้อมกัน ทำให้การประมวลผลแบบ Concurrent ทำได้ไม่เต็มประสิทธิภาพ
AsyncIO: การทำงานแบบ Non-Blocking
วิธีแก้ปัญหาแรกที่เราลองคือ AsyncIO ซึ่งเป็น Library ที่มาพร้อมกับ Python 3.7 ขึ้นไป AsyncIO ช่วยให้เราสามารถทำงานหลายอย่างพร้อมกันได้โดยไม่จำเป็นต้องรอให้แต่ละงานเสร็จสิ้นก่อน มันจะเหมือนมีคนหลายคนช่วยทำงาน แต่ละคนทำหน้าที่ของตัวเองไปเรื่อยๆ ถ้ามีงานที่ต้องรอ เช่น การโหลดข้อมูลจาก Web Server AsyncIO จะไม่หยุดรอ แต่จะไปทำงานอื่นต่อ แล้วค่อยกลับมาเช็คสถานะงานที่รออยู่เมื่อข้อมูลพร้อม
import asyncio
import aiohttp
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3"
]
tasks = [fetch_url(url) for url in urls]
results = await asyncio.gather(*tasks)
print(results)
if __name__ == "__main__":
asyncio.run(main())
ตัวอย่างนี้เราใช้ `asyncio.gather` เพื่อเรียก `fetch_url` สำหรับแต่ละ URL พร้อมกัน AsyncIO จะจัดการการทำงานของแต่ละ Task โดยไม่บล็อกการทำงานของโปรแกรม ทำให้โปรแกรมสามารถทำงานอื่นๆ ได้ระหว่างรอการโหลดข้อมูล จริงๆ ผมไม่ค่อยชอบวิธีนี้เพราะมันเพิ่มความซับซ้อนในการ Debug แต่ผลลัพธ์ที่ได้ก็คุ้มค่ามาก
Multiprocessing: การใช้ CPU หลาย Core
หลังจากใช้ AsyncIO แล้ว เราก็เริ่มมองหาทางเพิ่มประสิทธิภาพการประมวลผลแบบ CPU ด้วย วิธีที่ได้ผลดีที่สุดคือ Multiprocessing ซึ่งช่วยให้เราสามารถสร้าง Process หลายตัวที่ทำงานพร้อมกันโดยใช้ CPU หลาย Core ได้ แต่ละ Process จะมี Memory ของตัวเอง ทำให้ไม่เกิดปัญหา Race Condition ที่อาจเกิดขึ้นจากการใช้ Thread ร่วมกัน
import multiprocessing as mp
def process_data(data):
# ทำงานประมวลผลข้อมูล
result = data * 2
return result
if __name__ == "__main__":
data = [1, 2, 3, 4, 5]
with mp.Pool(processes=4) as pool:
results = pool.map(process_data, data)
print(results)
ในตัวอย่างนี้เราใช้ `multiprocessing.Pool` เพื่อสร้าง Pool ของ Process จำนวน 4 ตัว แต่ละ Process จะทำการประมวลผลข้อมูลใน List `data` `pool.map` จะส่งข้อมูลแต่ละชิ้นไปยัง Process แต่ละตัว เมื่อ Process ทำงานเสร็จแล้ว ผลลัพธ์จะถูกเก็บไว้ใน List `results` การใช้ Multiprocessing ช่วยให้เราสามารถประมวลผลข้อมูลจำนวนมากได้อย่างรวดเร็ว โดยใช้ประโยชน์จาก CPU หลาย Core ของเครื่อง
สิ่งที่ควรระวัง / ข้อผิดพลาดที่เจอบ่อย
การผสมผสาน AsyncIO และ Multiprocessing ต้องใช้ความระมัดระวัง ข้อผิดพลาดที่พบบ่อยคือ การจัดการ Memory ไม่ถูกต้อง เพราะแต่ละ Process จะมี Memory ของตัวเอง ถ้าเราไม่จัดการ Memory ให้ดี อาจทำให้เกิด Memory Leak หรือ Memory Corruption ได้ อีกข้อที่ต้องระวังคือ Synchronization Issues ถ้าเรามีหลาย Process ที่เข้าถึงและแก้ไขข้อมูลเดียวกันพร้อมกัน อาจเกิด Race Condition ได้ ดังนั้นเราต้องใช้ Locking Mechanisms หรือ Queues เพื่อจัดการการเข้าถึงข้อมูลอย่างปลอดภัย
นอกจากนี้ การเลือกใช้ AsyncIO กับ Multiprocessing ให้เหมาะสมก็สำคัญ AsyncIO เหมาะกับการทำงานที่ต้องรอ I/O เช่น การโหลดข้อมูลจาก Web Server หรือการอ่าน/เขียนไฟล์ ส่วน Multiprocessing เหมาะกับการทำงานที่ต้องใช้ CPU เป็นหลัก เช่น การประมวลผลข้อมูลจำนวนมาก การผสมผสานทั้งสองวิธีเข้าด้วยกันสามารถเพิ่มประสิทธิภาพของโปรแกรมได้อย่างมาก แต่ต้องออกแบบให้ถูกต้องและระมัดระวัง
สรุปและขั้นตอนต่อไป
จากการปรับแต่ง Script ด้วย AsyncIO และ Multiprocessing เราสามารถเพิ่มความเร็วในการรัน Script ได้ถึง 5 เท่า ผลลัพธ์ที่ได้ทำให้ทีมของเราสามารถทดสอบและปรับปรุงเกมได้เร็วขึ้นอย่างมาก ซึ่งส่งผลดีต่อกำหนดการปล่อยเกมโดยรวม ผมคิดว่าการใช้เทคนิคเหล่านี้เป็นสิ่งที่ทีมพัฒนาเกมทุกคนควรนำไปใช้ สำหรับขั้นตอนต่อไป ผมอยากจะลองศึกษาการใช้ AsyncIO กับ Kubernetes เพื่อให้สามารถ Deploy Application ได้อย่าง scalable และ resilient
คำถาม
คำถาม: AsyncIO และ Multiprocessing ใช้ได้กับ Python Version ไหนบ้าง?
คำตอบ: AsyncIO เริ่มมีประสิทธิภาพมากขึ้นเมื่อ Python 3.7 ขึ้นไป เนื่องจากมีการปรับปรุงในส่วนของ GIL และ Event Loop Multiprocessing รองรับ Python Version ได้หลากหลาย แต่ประสิทธิภาพจะดีที่สุดเมื่อใช้กับ Python 3.8 หรือสูงกว่า
คำถาม: ควรใช้ AsyncIO หรือ Multiprocessing ในกรณีไหน?
คำตอบ: ใช้ AsyncIO เมื่อต้องจัดการกับ I/O Bound Tasks (เช่น การรอข้อมูลจาก Network หรือ Disk) ใช้ Multiprocessing เมื่อต้องจัดการกับ CPU Bound Tasks (เช่น การคำนวณที่ซับซ้อน หรือการประมวลผลข้อมูลขนาดใหญ่)
คำถาม: มี Library อะไรบ้างที่ช่วยในการใช้งาน AsyncIO?
คำตอบ: มี Library หลายตัวที่ช่วยในการใช้งาน AsyncIO เช่น aiohttp (สำหรับการทำงานกับ Web Server), asyncio-taskqueue (สำหรับการจัดการ Tasks), และ aioserial (สำหรับการทำงานกับ Serial Port)