ทำไม GIL บน Python ถึงกำลังจะไม่ได้ไปต่อ นักพัฒนาได้อะไรจากมัน
By Arnon Puitrakul - 08 กุมภาพันธ์ 2026
ย้อนกลับไปเมื่อหลายปีก่อน เราเขียนเล่าเรื่อง GIL ที่ตอนนั้นเราบอกว่ามันเป็นเหมือน Limitation ของ Python ที่ทำให้เราสามารถทำงานได้ทีละ Thread เดียวเท่านั้น ถึงแม้ว่า CPU จะมีกี่ล้าน Core ก็ตาม ข้อดีของมันคือ การทำให้เราปลอดภัยจากการจัดการ Memory ที่ผิดพลาดได้ดีมาก ๆ แต่ในโลกปัจจุบันที่ Clock Speed มาถึงทางตัน เลยหันไปเพิ่ม CPU Core แทน และยุคสมัยของ AI ที่ต้องการ Parallel Computing มากขึ้น ทำให้ GIL จากเดิมเป็นฮีโร่ กลายเป็น คอขวด ทางวิศกรรมที่ใหญ่ที่สุดบน Python
แน่นอนว่า มันเป็นเรื่องใหญ่ขนาดนี้ มันมีความพยายามในการเอา GIL ออกมาหลายรอบมาก ๆ แล้ว GIL ก็น่าจะพูดออกมาว่า "ถ้าออกแล้วเอาอะไรแดก" จนกระทั่งการมาถึงของ Python 3.13 และ PEP 703 ที่เปรียบเสมือนการผ่าตัดใหญ่ที่สุดในประวัติศาสตร์กว่า 30 ปีของ Python เพื่อปลดล๊อคศักยภาพที่แท้จริงออกมา
ทำไมการเอา GIL ออกถึงยากมาก ๆ ?
หลาย ๆ คนอาจสงสัยว่า "แค่การเอา Lock ตัวนึงออก มันจะยากอะไรนักหนา ?" คำตอบซ่อนอยู่ในกลไกพื้นฐานที่สุดใน Python และเป็นส่วนที่ทำให้ Python ใช้งานง่ายมาก ๆ นั่นคือ ระบบการจัดการหน่วยความจำแบบ Reference Counting ทุกครั้งที่เราสร้างตัวแปร หรือ Object ขึ้นมา Python มันจะ แปะจำนวนเต็มเล็ก ๆ ไว้เสมอ เรียกว่า ob_refcnt เพื่อคอยนับอยู่ตลอดว่า ตอนนี้มีใคร Reference มาที่ ตัวแปร หรือ Object นี้บ้าง เมื่อเราส่งตัวแปรนี้ไปให้ Function มันจะบวก 1 และเมื่อทำงานเสร็จออกจาก Scope มันก็จะลบ 1 จนเมื่อ Garbage Collector หรือรถขยะเริ่มทำงาน มันจะเข้าไปหาว่า ตัวแปรไหนบ้างนะ ที่มีค่าตัวนับเป็น 0 แปลว่า มันไม่ได้ถูกใช้งานแล้ว ก็ให้เคลียร์ออกไป หรือ Deallocate ทันที
แต่ความเรียบง่ายนี้เอง มันเป็นกับดักชั้นดี เพราะในระบบ Multi-Thread หากไม่มี GIL กันไว้ และ ปล่อยให้ Thread มากกว่า 1 ตัวพยนายามเข้าไปอ่าน และแก้ไข ob_refcnt ของตัวแปรเดียวพร้อม ๆ กัน ชิบหายแน่นอน Race Condition มา ผลลัพธ์ ก็คือ หายนะระดับหมอลำลิงได้เลย เพราะตัวนับอาจทำงานผิดพลาด Memory Leak ได้เลย หรือหนักสุดคือ มันถูก Deallocate ไปแล้ว แต่ Thread อีกอันไม่รู้มันเรียกเข้าไป งานเข้า Segmentation Fault
เมื่อก่อน มันเคยมีความพยายามในการแก้ปัญหานี้อย่าง Project ที่ชื่อว่า "Gilectomy" เขาใส่ Lock เล็ก ๆ เข้าไปไว้ในทุก Object แทน GIL ที่เปรียบเสมือนแม่กุญแจดอกใหญ่ดอกเดียว แต่ผลลัพธ์ก็คือ Performance แย่ลงหนักมาก เพราะ CPU ต้องเสียเวลาไปกับการไขกุญแจ Lock นับล้านครั้งต่อวินาที ทำให้ Project นี้โดนดีดกลายเป็นฝุ่นไป
Biased Reference Counting คือทางออก ?
จุดเปลี่ยนที่สำคัญเกิดขึ้นเมื่อ Sam Gross วิศวกรจาก Meta ได้นำเสนอแนวทางใหม่ใน PEP 703 เรียกว่า Biased Reference Counting (BRC) แนวคิดนี้ตั้งอยู่บนข้อสังเกตของพฤติกรรมของโปรแกรม Python จริง ๆ ว่า Object ส่วนใหญ่มักจะถูกใช้งานโดย Thread เดียวกับที่สร้างมันขึ้นมา กล่าวคือ Thread ที่เป็นเจ้าของมักจะเข้าถึง Object นั้นบ่อยสุด ส่วน Thread อื่น ๆ นาน ๆ ทีถึงจะเข้ามายุ่งด้วย
แทนที่จะใช้ตัวนับเพียงตัวเดียวที่ต้องคอยแย่งกัน Lock ตัว BRC บอกว่า ให้แยกการนับออกเป็นโครงสร้างที่ซับซ้อนขึ้ โดยแบ่งสถานะการนับออกมาเป็น 2 โหมด โหมดแรกคือ Local Reference Count สำหรับ Thread ที่เป็นเข้าของ ซึ่งสามารถบวกหรือลบค่าได้ทันทีด้วยคำสั่ง CPU ปกติที่เร็วมาก ๆ ไม่ต้องมี Overhead ของการ Lock ใด ๆ ทั้งสิ้น คล้าย ๆ กับการทำงานแบบเดิมที่มี GIL โหมดที่ 2 คือ Shared Reference Count จะถูกใช้เมื่อมี Thread อื่น พยายามเข้าถึง Object นั้น โดย Thread อื่นจะต้องใช้วิธีการทาง Hardware อย่าง Atomic Operation เพื่อเปลี่ยนแปลงค่าตัวนับ เพื่อป้องกัน Race Condition ซึ่งแน่นอนว่า วิธีการนี้ช้ากว่าการแก้ตรง ๆ แน่ ๆ แต่มันแลกมาด้วยความปลอดภัยของข้อมูล
ความฉลาดของ BRC คือ การที่มัน Biased ไปทาง Thread ที่เป็นเจ้าของ Object ทำให้การทำงานส่วนใหญ่ยังเร็วเหมือนเดิม จะยอมเสีย Performance แค่ตอนที่เราทำงานข้าม Thread กันเท่านั้น นอกจากนี้ยังใช้เทคนิคเสริมอย่าง Deferred Refernece Counting สำหรับ Object บางประเภทที่ระบบจะไม่ทำการลบ หรือจัดการ Reference ทันทีที่มีการเปลี่ยนแปลง แต่จะรอรวบยอดเก็บที่เดียวเมื่อถึงช่วงเวลาที่เหมาะสม เช่นตอนที่ Garbage Collection ออกล่า เพื่อลดการใช้ Resouce นั่นเอง
Immortal Objects
อีกเทคนิคที่นำมาใช้ควบคู่กันเพื่อรีดประสิทธิภาพคือ Immortal Objects ตามชื่อเลยคือ การทำให้ Object บางตัวเป็นอมตะ ถ้าเราลองสังเกตดูเวลาเราเขียนโปรแกรม มันจะมี Object พื้นฐานบางตัวที่เราเรียกใช้โคตรบ่อย เช่น None, True และ False ใช่แล้ว จริง ๆ มันคือ Object นะ หรือกระทั่งพวก จำนวนเต็มขนาดเล็กอย่าง 1 และ 0
ปกติถ้าเราเรียกใช้ Object พวกนี้ โปรแกรมจะต้องเสียเวลาจัดการ Refernece count ซึ่งเป็นการสิ้นเปลือง Resource โดยเปล่าประโยชน์มาก ๆ เพราะเรารู้อยู่แล้ว่า Object พวกนี้ไม่มีวันถูกทำลายอยู่แล้ว
ทำให้ใน Python 3.13 แบบ Free-Threaded ตัว Object เหล่านี้ จะถูกกำหนดค่า Refernece Count เป็นค่าพิเศษที่ระบบรู้กันว่า ไม่ต้องมายุ่ง ทำให้ Thread ต่าง ๆ สามารถหยิบพวก None หรือ True มาใช้งานได้โดยไม่ต้องเสียเวลาตรวจสอบอะไรทั้งสิ้น อ่านดูอาจจะมองว่าเป็นเรื่องเล็กน้อย แต่อย่าลืมนะว่า ในโปรแกรมใหญ่ ๆ เราเรียก Object พวกนี้เยอะโคตร ๆ ส่งผลทำให้ สามารถลด Overhead โดยรวมได้เยอะมาก ๆ และคิดว่าเป็นส่วนสำคัญที่ทำให้ Python แบบไม่มี GIL ทำงานเร็วพอที่จะรับได้
เมื่อกำแพงพังทลาย จะเกิดอะไรขึ้น
เมื่อกำแพง GIL พังทลายลง สิ่งที่เราได้มาคือ ความสามารถในการทำ Parallelism จริง ๆ ไม่ใช่แค่การสลับ Thread ไป ๆ มา ๆ เหมือนเมื่อก่อนแล้ว แต่เป็น Python Script ที่รันบน Core ที่ 1,2,3 และ 4 พร้อมกันได้จริง ๆ จากเดิมที่ ถ้าเราต้องการรีดทุก Core ออกมา เราจะต้องใช้ multiprocessing ซึ่งเบื้องหลังคือ เราจะต้อง Fork Process ใหม่ออกจากกัน และถ้าต้องมีการ Copy ข้อมูลไป ๆ มา ๆ มันจะต้องใช้ Pickling/Unpickling ซึ่งกิน RAM หนัก และช้ามาก
แต่ในโลกที่ไม่มี GIL เราสามารถใช้ threading Module แบบเดิม แต่ผลลัพธ์ที่ได้คือ หน้ามือหลังเท้าเลย Thread ต่าง ๆ มาสามารถเข้าถึง Memory ก้อนเดียวกันได้ โดยไม่ต้อง Copy ข้อมูล งานที่มันเป็นพวก CPU-Bound อย่าง การทำ Image Processing, AI Model Training/Inference จะได้เห็น Performance เพิ่มขึ้นเกือบจะเป็น Linear เมื่อเราเพิ่ม CPU Core เข้าไป นอกจากนี้งานที่เป็นกลุ่ม I/O Bound จากเดิมที่ต้องรอแล้วสลับไปสลับมา ก็ไม่ต้องเสีย Overhead ในการสลับแล้ว ใครรอก็รอไป โดยรวมก็จะเร็วขึ้นอีก
อิสรภาพมีราคาที่ต้องจ่าย
การเอา GIL ออกไป มันมีราคาที่ต้องจ่ายเหมือนกันนะ ไม่มีอะไรได้มาฟรี ๆ หรอกนะ อย่างแรกคือ ทำให้ Performance ของ Single-Thread ลดลง จากการทดสอบเบื้องต้นพบว่า Python 3.13 แบบที่เอา GIL ออกไป จะทำงานช้าลงประมาณ 10-15% เมื่อเทียบกับ Version ปกติ เหตุผลเกิดจาก ถึงแม้ว่า BRC จะช่วย แต่ CPU จะต้องทำงานซับซ้อนขึ้นในการเช็คสถานะของ Object และจัดการ Memory แทนที่จะเช็คแค่ GIL เหมือนตัวปกติ
อย่างที่ 2 คือ ความเข้ากันได้ของ Ecosystem ทุกคนรู้ โลกรู้ว่า Python มี Ecosystem ที่โคตรใหญ่ ทำให้มันแทบจะ Dev ทุกอย่างได้ง่ายมาก ๆ ปัญหามันไปตกกับพวก Library ที่เขียนด้วยภาษา C เช่น กลุ่มกระดูกสันหลังทุกคนใช้อย่าง Numpy, Pandas หรือกระทั่ง PyTorch ที่ถูกเขียนขึ้นมาโดยมีสมมุติฐานว่า เขามี GIL คุ้มหัวอยู่ ทำให้เขาไม่ได้เขียนมาให้มัน Thread-Safe เลย การเอา GIL ออก อาจทำให้มันทำงานผิดพลาด หรือพังไปเลยก็ได้ ทำให้นักพัฒนา Library ต้องรื้อ Code ฉ่ำ เพื่อรองรับการเปลี่ยนแปลงครั้งนี้ และในช่วงเปลี่ยนผ่านนี้ เราอาจจะปวดหัวกับการใช้พวก Binary Wheel ที่แยก Version ปกติ และ Free-Threaded
สรุป
การเปลี่ยนแปลงใน Python 3.13 ถือว่าเป็นเพียงการทดสอบ อยู่ในขั้นตอน Experimental Stage เท่านั้น ที่ต้องเข้าไปเปิดใช้งานเอง ไม่ใช่ค่าเริ่มต้น เป้าหมายของทีมพัฒนา Core Python คือการค่อย ๆ ปรับปรุง Performance ของ Single-Thread ให้กลับมาดีใกล้เคียงเดิมที่สุด และรอให้ Ecosystem ปรับตัวให้ทัน ซึ่งเราคิดว่าอย่างน้อยต้องใช้เวลา 5 ปีหรือมากกว่านั้น กว่าที่ GIL จะเอาออกได้อย่างสมบูรณ์เป็นค่าเริ่มต้น เรื่องที่เราชื่นชม Python ในการตัดสินใจเอา PEP 703 เข้ามา คือ มันเป็นการตัดสินใจที่ทำให้รู้ว่า Python เอง ก็ปรับตัวเข้ากับ Hardware ยุคใหม่ พร้อมที่จะเป็นภาษาที่ยังสามารถครองใจนักพัฒนาต่อไปได้ และไม่ยอมให้ตัวเองเป็นภาษาที่ช้าเพราะข้อจำกัดของตัวเองอีกต่อไป และเราก็คิดว่า ช่วงนี้เป็นช่วงเวลาที่น่าตื่นเต้นนะ ที่เราจะได้เห็นการเปลี่ยนแปลงนี้



