บทที่ 6.2: Threading และ Multiprocessing (การทำงานแบบขนานและการรับมือกับ GIL)
เอกสารนี้อธิบายเกี่ยวกับแนวคิดการประมวลผลแบบทำงานพร้อมกัน (Concurrency) และการทำงานแบบขนาน (Parallelism) ในภาษา Python กลไก Global Interpreter Lock (GIL) การเลือกใช้งานโมดูล threading สำหรับงาน I/O-bound และโมดูล multiprocessing สำหรับงาน CPU-bound
1. Global Interpreter Lock (GIL) คืออะไร
CPython (ตัวตีความภาษา Python มาตรฐาน) มีกลไกการล็อกระดับหน่วยความจำที่เรียกว่า Global Interpreter Lock (GIL) ซึ่งอนุญาตให้ Thread ทำงานประมวลผลคำสั่ง Python ได้ทีละ 1 Thread เท่านั้นในหนึ่งช่วงเวลา เพื่อความปลอดภัยในการจัดการหน่วยความจำ (Thread-Safety)
ผลกระทบของ GIL ต่อประสิทธิภาพการทำงาน:
- I/O-Bound Tasks: (การรออ่าน/เขียนไฟล์, อ่านข้อมูลจาก Network/Database) Thread สามารถคืนสิทธิ์ GIL ให้ Thread อื่นทำงานระหว่างรอ I/O ได้ จึงใช้ Threading เพิ่มความเร็วได้ดี
- CPU-Bound Tasks: (การคำนวณคณิตศาสตร์ซับซ้อน, การประมวลผลภาพ/วิดีโอ) GIL จะบล็อกไม่ให้ Thread ทำงานแบบขนานบนหลาย CPU Core ได้จริง จึงต้องใช้ Multiprocessing เพื่อรัน Python Process แยกกันแทน
2. การใช้งาน Threading (สำหรับ I/O-Bound Tasks)
โมดูล threading เหมาะสำหรับการทำงานที่ใช้เวลาส่วนใหญ่ไปกับการรอการตอบกลับจากภายนอก
import threading
import time
def download_file(file_id):
print(f"กำลังเริ่มดาวน์โหลดไฟล์ {file_id}...")
time.sleep(2) # จำลองการทำงานแบบ I/O-bound (รอระบบ)
print(f"ดาวน์โหลดไฟล์ {file_id} เสร็จสิ้น")
threads = []
# สร้างและสั่งทำงาน 3 Threads พร้อมกัน
for i in range(1, 4):
t = threading.Thread(target=download_file, args=(i,))
threads.append(t)
t.start()
# รอให้ทุก Thread ทำงานเสร็จก่อนไปบรรทัดถัดไป
for t in threads:
t.join()
print("ดาวน์โหลดไฟล์ทั้งหมดเรียบร้อยแล้ว")
3. การใช้งาน Multiprocessing (สำหรับ CPU-Bound Tasks)
โมดูล multiprocessing จะสร้าง Python Process แยกกัน ซึ่งแต่ละ Process จะมีตัวตีความ Python และหน่วยความจำของตัวเอง ทำให้หลบเลี่ยงข้อจำกัดของ GIL และใช้ประโยชน์จาก CPU Multi-core ได้อย่างเต็มที่
import multiprocessing
import time
def cpu_heavy_task(number):
# จำลองการคำนวณที่ใช้ CPU หนัก
count = 0
for i in range(10000000):
count += i * number
return count
if __name__ == "__main__":
processes = []
# สร้าง Process แยกทำงานบนคนละ CPU Core
for i in range(1, 4):
p = multiprocessing.Process(target=cpu_heavy_task, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
print("ประมวลผลคำนวณทุก Process เสร็จสิ้น")
ข้อควรระวัง: การใช้งาน
multiprocessingจำเป็นต้องครอบโค้ดหลักด้วยif __name__ == "__main__":เสมอ เพื่อป้องกันไม่ให้สร้าง Process ซ้อนกันแบบไม่จบสิ้น (Process Spawning Loop)
4. การจัดการด้วย concurrent.futures (High-Level API)
โมดูล concurrent.futures ให้ Interface ที่ใช้งานง่ายผ่าน ThreadPoolExecutor และ ProcessPoolExecutor ในการจัดการ Thread/Process Pool และรับค่าคืนกลับ (Return Value)
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def square(n):
return n * n
numbers = [1, 2, 3, 4, 5]
# ตัวอย่างการใช้ ThreadPoolExecutor (สำหรับ I/O-Bound)
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(square, numbers))
print("ผลลัพธ์:", results) # Output: [1, 4, 9, 16, 25]
5. ตารางเปรียบเทียบ Threading vs Multiprocessing
| คุณสมบัติ | Threading | Multiprocessing |
|---|---|---|
| ขอบเขตการทำงาน | แชร์ Memory ร่วมกันใน Process เดียว | แยก Memory Space อิสระต่อกัน |
| ผลกระทบจาก GIL | ถูกจำกัดโดย GIL | ไม่ถูกจำกัดโดย GIL |
| เหมาะสำหรับงาน | I/O-Bound (Web Scraping, Network Request) | CPU-Bound (Data Crunching, Image Processing) |
| Overhead ในการสร้าง | น้อย (Lightweight) | สูงกว่า (ต้องจอง Memory และ Spawning ใหม่) |
| การสื่อสารระหว่างกัน | ทำได้ง่าย (ใช้ตัวแปรใน Process เดียวกัน) | ต้องใช้ IPC (Queue, Pipe, Shared Memory) |