GIL — Global Interpreter Lock
JavaScript ใช้ single thread — Python มี thread แต่มีข้อแม้
หัวข้อที่มีชื่อว่า “JavaScript ใช้ single thread — Python มี thread แต่มีข้อแม้”ในฐานะนักพัฒนา TypeScript คุณคุ้นเคยกับโมเดล concurrency ของ JavaScript อยู่แล้ว: หนึ่ง thread, หนึ่ง event loop, non-blocking I/O ผ่าน callback และ async/await ไม่มี data race เกิดขึ้นเพราะมีโค้ด JavaScript รันได้แค่ชิ้นเดียวในเวลาหนึ่ง ๆ เท่านั้น worker threads มีอยู่จริง แต่โดย default จะไม่แชร์ memory กัน — สื่อสารกันผ่าน message passing
Python มี OS thread จริงผ่านโมดูล threading คุณ spawn หลาย thread ได้และมันรันอยู่ใน process เดียวกันโดยแชร์ memory ร่วมกัน อย่างไรก็ตาม CPython (Python implementation หลัก) มีกลไกที่เรียกว่า Global Interpreter Lock ซึ่งจำกัดสิ่งที่ thread เหล่านั้นทำแบบขนานได้จริง ๆ การเข้าใจ GIL จึงสำคัญมากสำหรับการเขียนโปรแกรม Python ที่ต้อง scale ข้าม CPU core
GIL คืออะไร
หัวข้อที่มีชื่อว่า “GIL คืออะไร”GIL คือ mutex — mutual exclusion lock — ที่ปกป้อง internal state ของ CPython การจัดการ memory ของ CPython (reference counting สำหรับ garbage collection) ไม่ thread-safe แทนที่จะเพิ่ม lock แบบละเอียดทั่ว interpreter ทีมของ Guido van Rossum เลือกใช้ lock หยาบตัวเดียว: มีแค่ thread เดียวเท่านั้นที่รัน Python bytecode ได้ในแต่ละขณะ
Thread จะสลับกันทำงาน — GIL ถูกปล่อยและ acquire ใหม่เป็นระยะ (ประมาณทุก 5ms ใน Python 3.2+ หรือเมื่อ thread เรียก I/O system call) แต่สำหรับงาน CPU ล้วน ๆ การมีสอง thread ไม่ได้ให้ throughput เท่ากับสอง CPU core thread หนึ่งรัน อีก thread รอ
// TypeScript/Node.js: worker_threads for CPU parallelism// Workers share no memory by default — communicate with postMessageimport { Worker, isMainThread, parentPort } from 'worker_threads';
if (isMainThread) { const worker = new Worker(__filename); worker.on('message', (result) => { console.log('result from worker:', result); }); worker.postMessage({ start: 0, end: 1_000_000 });} else { parentPort?.on('message', ({ start, end }) => { let sum = 0; for (let i = start; i < end; i++) sum += i; parentPort?.postMessage(sum); });}# Python: threads exist but the GIL limits CPU parallelismimport threading
results = []
def cpu_sum(start, end, out, index): total = sum(range(start, end)) out[index] = total
# Two threads — but only one runs Python bytecode at a timet1 = threading.Thread(target=cpu_sum, args=(0, 500_000, results, 0))t2 = threading.Thread(target=cpu_sum, args=(500_000, 1_000_000, results, 1))results = [None, None]
t1.start(); t2.start()t1.join(); t2.join()
print(sum(results)) # correct answer, but not 2x fasterเมื่อ GIL เป็นปัญหา และเมื่อไม่เป็น
หัวข้อที่มีชื่อว่า “เมื่อ GIL เป็นปัญหา และเมื่อไม่เป็น”GIL บล็อกเฉพาะการรัน Python bytecode พร้อมกันเท่านั้น การทำงานหลายอย่างที่ดูเหมือนโค้ด Python จริง ๆ แล้วลงไปที่ C extension หรือ OS ซึ่งสามารถปล่อย GIL ได้ตามใจสมัคร:
| Workload | ผลของ GIL | เครื่องมือที่แนะนำ |
|---|---|---|
| CPU-bound Python loop | บล็อก parallelism | multiprocessing หรือ C extension |
| Network I/O (requests, sockets) | GIL ถูกปล่อยระหว่าง syscall | threading หรือ asyncio |
| File I/O | GIL ถูกปล่อยระหว่าง syscall | threading หรือ asyncio |
| NumPy / pandas operation | GIL ถูกปล่อยใน C layer | threading ใช้ได้สบาย |
| Subprocess call | GIL ถูกปล่อย | threading ใช้ได้สบาย |
กฎที่ใช้ได้จริง: ถ้า bottleneck คือ Python interpreter กำลังรันโค้ดของคุณ thread จะไม่ช่วย — ใช้ multiprocessing ถ้า bottleneck คือการรอระบบภายนอก (network, disk, database) thread หรือ asyncio ใช้ได้ดี
หลบ GIL ด้วย multiprocessing
หัวข้อที่มีชื่อว่า “หลบ GIL ด้วย multiprocessing”multiprocessing spawn Python interpreter process แยกต่างหาก — แต่ละตัวมี GIL และ CPU core ของตัวเอง นี่คือเครื่องมือมาตรฐานสำหรับ CPU-bound parallelism
// TypeScript: CPU parallelism via worker_threads poolimport { Worker } from "worker_threads";import * as os from "os";
function runWorker(data: number): Promise<number> { return new Promise((resolve, reject) => { const w = new Worker(` const { parentPort, workerData } = require("worker_threads"); let total = 0; for (let i = 0; i < workerData; i++) total += i; parentPort.postMessage(total); `, { eval: true, workerData: data }); w.on("message", resolve); w.on("error", reject); });}
const cpus = os.cpus().length;Promise.all(Array.from({ length: cpus }, (_, i) => runWorker(1_000_000 / cpus))).then(results => console.log(results.reduce((a, b) => a + b, 0)));# Python: multiprocessing bypasses the GIL entirelyfrom multiprocessing import Poolimport os
def cpu_work(n): return sum(range(n))
if __name__ == "__main__": cpus = os.cpu_count() chunk = 1_000_000 // cpus with Pool(cpus) as pool: results = pool.map(cpu_work, [chunk] * cpus) print(sum(results))Free-threaded Python เป็นทางการแล้ว (3.14)
หัวข้อที่มีชื่อว่า “Free-threaded Python เป็นทางการแล้ว (3.14)”Python 3.13 ปล่อย GIL-free build ออกมาเป็นการทดลอง (PEP 703, python3.13t) Python 3.14 เลื่อนขั้นให้เป็น build ที่รองรับอย่างเป็นทางการ (PEP 779): ยังเป็น interpreter แยกต่างหากชื่อ python3.14t และยังไม่ใช่ค่า default — แต่ไม่ใช่ของทดลองอีกต่อไป ใน build นี้ thread รัน Python bytecode บนหลาย core พร้อมกันได้จริง
ราคาที่ต้องจ่ายมีจริง: reference counting เปลี่ยนไปใช้ atomic operation โค้ด single-thread จึงช้าลงราว 5–10% และ C extension ที่แอบพึ่ง GIL อยู่ต้องถูกตรวจสอบและ build ใหม่ เช็กว่ากำลังรัน build ไหนได้ด้วย sys._is_gil_enabled()
Python 3.14 ยังเพิ่ม concurrent.interpreters (PEP 734) — หลาย interpreter ในหนึ่ง process แต่ละตัวมี GIL ของตัวเอง พร้อม InterpreterPoolExecutor ที่เสียบเข้ากับ API ของ concurrent.futures ได้เลย มันอยู่ตรงกลางระหว่าง thread (แชร์ memory, GIL เดียว) กับ process (ไม่แชร์ memory, เริ่มช้า) และเป็นสิ่งที่ใกล้เคียง worker_threads ของ Node มากที่สุดเท่าที่ Python มี
หมายเหตุเรื่อง Playground: การสาธิตผลของ GIL ต้องเปรียบเทียบเวลาของงาน CPU-bound กับ I/O-bound ซึ่งไม่มีความหมายในแซนด์บ็อกซ์บนเบราว์เซอร์ ให้รันตัวอย่างในเครื่องด้วย
python3 -c "import threading; ..."