ข้ามไปยังเนื้อหา

GIL — Global Interpreter Lock

ในฐานะนักพัฒนา 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 คือ 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
// TypeScript/Node.js: worker_threads for CPU parallelism
// Workers share no memory by default — communicate with postMessage
import { 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
# Python: threads exist but the GIL limits CPU parallelism
import 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 time
t1 = 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 บล็อกเฉพาะการรัน Python bytecode พร้อมกันเท่านั้น การทำงานหลายอย่างที่ดูเหมือนโค้ด Python จริง ๆ แล้วลงไปที่ C extension หรือ OS ซึ่งสามารถปล่อย GIL ได้ตามใจสมัคร:

Workloadผลของ GILเครื่องมือที่แนะนำ
CPU-bound Python loopบล็อก parallelismmultiprocessing หรือ C extension
Network I/O (requests, sockets)GIL ถูกปล่อยระหว่าง syscallthreading หรือ asyncio
File I/OGIL ถูกปล่อยระหว่าง syscallthreading หรือ asyncio
NumPy / pandas operationGIL ถูกปล่อยใน C layerthreading ใช้ได้สบาย
Subprocess callGIL ถูกปล่อยthreading ใช้ได้สบาย

กฎที่ใช้ได้จริง: ถ้า bottleneck คือ Python interpreter กำลังรันโค้ดของคุณ thread จะไม่ช่วย — ใช้ multiprocessing ถ้า bottleneck คือการรอระบบภายนอก (network, disk, database) thread หรือ asyncio ใช้ได้ดี

multiprocessing spawn Python interpreter process แยกต่างหาก — แต่ละตัวมี GIL และ CPU core ของตัวเอง นี่คือเครื่องมือมาตรฐานสำหรับ CPU-bound parallelism

TypeScript
// TypeScript: CPU parallelism via worker_threads pool
import { 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
# Python: multiprocessing bypasses the GIL entirely
from multiprocessing import Pool
import 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))

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; ..."

GIL ป้องกันอะไรใน CPython?
GIL สร้างปัญหามากที่สุดกับ workload ประเภทใด?
โมดูลใดของ Python เป็นคำตอบมาตรฐานสำหรับ CPU parallelism จริงข้ามหลาย core?
asyncio ทำให้เกิด concurrency ได้อย่างไรโดยไม่ใช้ thread?