InterpreterPoolExecutor: ใช้หลาย Core โดยไม่แยก Process
ThreadPoolExecutor ใช้ง่ายสำหรับ I/O แต่โค้ด Python ที่กิน CPU มักติด GIL ส่วน ProcessPoolExecutor ใช้หลาย core ได้แต่สร้าง process และส่งข้อมูลมีต้นทุนครับ Python 3.14 เพิ่ม InterpreterPoolExecutor เป็นทางเลือกกลาง: worker แต่ละตัวอยู่ใน thread แต่มี interpreter และ GIL ของตัวเอง
WHY: Parallelism ต้องแลกกับ Isolation
แต่ละ interpreter มี sys.modules, builtins และ global state แยกกัน การ import ใน worker หนึ่งไม่ทำให้อีก worker เห็น เหมือนมีครัวหลายห้องในอาคารเดียวกัน อาหารทำพร้อมกันได้ แต่ห้ามหยิบหม้อ mutable ข้ามกำแพงโดยตรง
executor serialize callable, arguments และ return value ด้วย pickle จึงต้องใช้ฟังก์ชัน top-level และข้อมูลที่ pickle ได้ Lambda, closure ที่จับ resource, open socket และ object native บางชนิดไม่เหมาะ หาก extension module ไม่รองรับ subinterpreter isolation อาจทำงานผิดหรือปฏิเสธโหลด ต้องทดสอบ dependency ทุกตัว
import concurrent.futures
import unittest
def count_divisors(value):
return sum(1 for candidate in range(1, value + 1) if value % candidate == 0)
class InterpreterPoolTests(unittest.TestCase):
def test_runs_picklable_cpu_jobs(self):
values = [12, 15, 16]
with concurrent.futures.InterpreterPoolExecutor(max_workers=2) as pool:
result = list(pool.map(count_divisors, values))
self.assertEqual(result, [6, 4, 5])
if __name__ == "__main__":
unittest.main()
HOW: เลือก Pool จากคอขวดจริง
งาน network หรือ disk ที่ library ปล่อย GIL ใช้ thread pool ง่ายสุด งาน CPU ที่ต้องพึ่ง extension มากและ isolation ชัดใช้ process pool เป็น baseline ส่วน interpreter pool เหมาะกับ pure-Python CPU task ที่ข้อมูลเข้าออกไม่ใหญ่และ dependency รองรับ
benchmark ทั้งเวลารวมและ serialization cost ด้วย payload จริง ถ้างานต่อชิ้นสั้นกว่าเวลาส่งข้อมูล ให้ batch หลายรายการต่อ task หรือกลับไปใช้ sequential loop อย่าตั้ง max_workers ตามจำนวน request; เริ่มจากจำนวน core ที่มี quota แล้ววัด contention
exception จาก worker พยายามรักษาชนิดเดิมและแนบ ExecutionFailed เป็น cause แต่ log pipeline ควรเก็บทั้งสองชั้น ใช้ context manager เพื่อ shutdown เสมอ และตั้ง timeout/cancellation ใน orchestration เพราะ executor ไม่รู้ deadline ทางธุรกิจเอง
Recap
- แต่ละ subinterpreter มี GIL และ runtime state แยก
- callable/argument/result ต้องข้าม pickle boundary
- ทดสอบ native extension กับ subinterpreter โดยเฉพาะ
- เปรียบเทียบ thread, interpreter และ process ด้วย workload จริง
- batch งานสั้นและจำกัด worker ตาม CPU quota
พรุ่งนี้หยิบ pure-Python CPU job หนึ่งตัวมาวัดสาม executor แบบ input เดียวกัน แล้วเลือกจาก p95 และ memory ครับ