Skip to content

บทที่ 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)