gh-116167: อนุญาตให้ปิดใช้งาน GIL
(github.com/python)- CPython PR #116338 ได้ merge การเปลี่ยนแปลงที่ทำให้สามารถปิดใช้งาน GIL ได้ใน free-threaded build ด้วย
PYTHON_GIL=0หรือ-X gil=0เข้าไปยังpython:main - เพื่อเปิดทางให้สามารถเปิด GIL กลับมาได้อีกใน runtime จึงมีการ initialize โครงสร้างข้อมูลที่เกี่ยวข้องกับ GIL ตามปกติเหมือนเดิม และจัดการการปิดใช้งานด้วยการตั้งค่าแฟลกตอนเริ่มต้นเพื่อให้
take_gil()และdrop_gil()return ออกก่อน - จากการตรวจสอบเบื้องต้น เมื่อตั้งค่า
PYTHON_GIL=0พบว่าบางเทสต์และโปรแกรมขนาดเล็กที่ไม่ใช้เธรดทำงานได้ตามปกติ และโปรแกรมเธรดที่พื้นฐานมาก ๆ ก็ทำงานได้เป็นบางครั้ง แต่ test suite ทั้งชุด crash อย่างรวดเร็วที่test_asyncio - ระหว่างการรีวิว ได้มีการเพิ่มเทสต์ของ
PYTHON_GIL, เอกสารประกอบ, ตัวเลือก-X gilและการสะท้อนค่าในsys.flagsรวมถึงแก้การจัดการการตั้งค่าให้PYTHON_GIL=1บังคับ การเปิดใช้งาน GIL - งานต่อเนื่องถูกแยกออกเป็นประเด็นการเปิด GIL กลับเมื่อโหลดส่วนขยายที่ไม่เข้ากัน และประเด็นการปิด GIL เป็นค่าเริ่มต้น โดยการเปลี่ยนแปลงนี้เพิ่มพื้นผิวการควบคุม GIL ใน free-threaded build ของ Python 3.13
การเปลี่ยนแปลงที่ merge แล้ว
- CPython PR #116338 ครอบคลุมการเปลี่ยนแปลง
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 colesburyเป็นผู้ merge เข้าpython:mainเมื่อวันที่ 11 มีนาคม 2024- ขนาดของการเปลี่ยนแปลงแสดงเป็น 12 ไฟล์, เพิ่ม 163 บรรทัด, ลบ 1 บรรทัด
- ฟีเจอร์เป้าหมายคือออปชันขณะรันเพื่อปิดใช้งาน GIL ใน free-threaded build ไม่ใช่ใน build ปกติ
วิธีปิดใช้งาน GIL
- ใน free-threaded build สามารถปิดใช้งาน GIL ได้ด้วยการตั้งค่าต่อไปนี้
PYTHON_GIL=0-X gil=0
- เพื่อให้สามารถเปิด GIL กลับมาได้ใน runtime โครงสร้างข้อมูลที่เกี่ยวข้องกับ GIL ทั้งหมดจะถูก initialize ตามปกติ
- การปิดใช้งานจริงใช้วิธีตั้งค่าแฟลกตอนเริ่มต้น
- แฟลกนี้ทำให้
take_gil()และdrop_gil()return ออกก่อน
- แฟลกนี้ทำให้
- ระหว่างการรีวิว ยังมี commit เพิ่มเติมเพื่อกำหนด
enable_gilให้ถูกต้องเมื่อPYTHON_GIL=1
การทดสอบและข้อจำกัดในปัจจุบัน
- มีการตรวจสอบบางเทสต์และโปรแกรมขนาดเล็กด้วยการตั้งค่า
PYTHON_GIL=0- เทสต์และโปรแกรมขนาดเล็กที่ไม่ใช้เธรดได้รับการยืนยันว่าทำงานได้ตามปกติ
- โปรแกรมเธรดที่พื้นฐานมาก ๆ ทำงานได้เป็นบางครั้ง
- test suite ทั้งชุด crash อย่างรวดเร็ว และบันทึกตำแหน่งไว้ที่
test_asyncio - มีการจอง builder test ที่เกี่ยวข้องกับ NoGIL หลายครั้งด้วยคำสั่ง
!buildbot nogilx86-64 MacOS Intel ASAN NoGIL PRx86-64 MacOS Intel NoGIL PRARM64 MacOS M1 Refleaks NoGIL PRARM64 MacOS M1 NoGIL PRAMD64 Ubuntu NoGIL Refleaks PRAMD64 Ubuntu NoGIL PRAMD64 Windows Server 2022 NoGIL PR
ขอบเขตที่เพิ่มเข้ามาระหว่างการรีวิว
corona10เสนอว่าน่าจะคุ้มค่าที่จะเพิ่มการทดสอบ environment variable ในLib/test/test_cmd_line.py- หลังจากนั้นมี commit ต่อไปนี้ถูกเพิ่มเข้ามา
Add test for PYTHON_GIL in test_cmd_lineSet enable_gil properly when PYTHON_GIL=1Don't add 'enable_gil' to test_embed in normal builds
colesburyมองว่าควรจัดทำเอกสารพร้อมกับช่วงที่เพิ่ม environment variable- โดยอ้างอิงว่ามีการจัดทำเอกสารสำหรับ configure flag
--disable-gilอยู่แล้ว - เนื้อหาเอกสารควรระบุว่าสามารถใช้ได้เฉพาะใน free-threaded build,
0คือบังคับปิด GIL,1คือบังคับเปิด GIL และเป็นสิ่งใหม่ใน Python 3.13
- โดยอ้างอิงว่ามีการจัดทำเอกสารสำหรับ configure flag
- หลังจากนั้นได้มี commit
Document PYTHON_GIL environment variableเพิ่มเข้ามา
การเพิ่มตัวเลือก -X gil และการ merge ขั้นสุดท้าย
- หลังจากการพูดคุยบน Discord จึงตัดสินใจเพิ่ม ตัวเลือก
-Xที่ใช้ร่วมกับ environment variable ด้วย - ชื่อ PR ถูกเปลี่ยนจากรูปแบบที่กล่าวถึงแค่
PYTHON_GIL=0ให้ครอบคลุมถึงPYTHON_GIL=0 or -X gil=0ด้วย - commit ที่เพิ่มเข้ามามีเนื้อหาต่อไปนี้
Add -X gil option, add to sys.flags, modify test to cover env var… and optionFix link to -X gilFix PYTHON_GIL versionchanged lineClarify test_flags in normal builds
ericsnowcurrently,erlend-aasland,corona10,colesburyอนุมัติการเปลี่ยนแปลงนี้- merge commit คือ
2731913และหลัง merge แล้วvstinnerตอบสนองต่อการเปลี่ยนแปลงนี้ว่า “น่าสนใจและน่ากลัวมาก”
งานต่อเนื่อง
- มีการแยกงานต่อเนื่องออกเป็น 2 ประเด็น
- PR ปัจจุบันนี้ไม่ใช่การเปลี่ยนค่าเริ่มต้นของ GIL แต่เป็นการเปลี่ยนแปลงที่ทำให้ผู้ใช้ใน free-threaded build สามารถควบคุมสถานะ GIL ได้ผ่าน environment variable หรือออปชัน
-X
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ฝากลิงก์เพิ่มเติมไว้สำหรับคนที่สนใจงาน no-GIL: [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Github repo
https://github.com/colesbury/nogil
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
น่าตื่นเต้นว่าจะทำให้ Python พื้นฐานเร็วขึ้นได้อีกแค่ไหน คุณค่าหลักของ Python เองก็กำลังถูกท้าทาย เพราะมีเครื่องมือออกมาช่วยบรรเทาปัญหานี้มากเกินไป
เครื่องมือเพิ่มความเร็วที่นึกออกคือ Mojo, pytorch, triton, numba, taichi มีความพยายามแก้ปัญหานี้เยอะมาก จนครั้งก่อนที่ลองจะใช้สักตัว รู้สึกท่วมท้นเพราะตัวเลือกเยอะเกินไป สุดท้ายเลือก taichi ซึ่งค่อนข้างสนุกและใช้ง่าย แต่ขอบเขตการใช้งานค่อนข้างจำกัด
Taichi ถูกประเมินต่ำเกินไปจริง ๆ มันทำงานได้บนทุกแพลตฟอร์มรวมถึง Metal มีตัวอย่างเยอะ และเขียนโค้ดง่าย ที่สำคัญที่สุดคือมันผสานเข้ากับ ecosystem และไม่ได้มาแทนที่ ecosystem เดิม
https://github.com/taichi-dev
วิดีโอเดโมดี ๆ ที่แสดงให้เห็นว่าใช้ Taichi ทำอะไรได้บ้าง: https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
สงสัยว่าทำไมวิธี biased reference counting ที่อธิบายใน https://peps.python.org/pep-0703/ ถึงให้ความเป็นมิตรกับเธรดเดียวเท่านั้น และถ้าเข้าถึงจากเธรดอื่นก็ต้องเพิ่ม/ลดแบบ atomic
ใน implementation อื่น ๆ เช่น Rust crates หลายตัวที่ทำ biased reference counting เคยเห็นวิธีที่เพิ่มแบบ atomic เฉพาะตอนย้ายไปเธรดใหม่ จากนั้นเธรดนั้นจะเพิ่ม/ลดแบบ non-atomic ไปจนกว่าจะกลับถึง 0 แล้วค่อยลดแบบ atomic ครั้งสุดท้าย สงสัยว่าเป็นเพราะมันเป็นการต่อเติมเข้ากับระบบเดิม จึงมี PyObject เดี่ยวอยู่แล้ว และไม่สามารถสลับไปชี้ object แบบ thread-local ตัวใหม่ได้หรือเปล่า
ใน Rust การ "move" เพื่อโอน ownership เป็นส่วนหนึ่งของภาษา แต่ใน C หรือ Python ไม่มีแนวคิดที่เทียบเท่ากัน จึงยากที่จะตัดสินว่าเมื่อไรควรโอน ownership และเธรดไหนควรเป็นเจ้าของใหม่ ใช้ heuristic ได้ เช่น อาจสละหรือโอน ownership ตอนใส่ object ลงใน queue.SimpleQueue แต่ถึงอย่างนั้นก็ยังยากที่จะรู้ล่วงหน้าว่าเธรดไหนจะ "get" object ที่อยู่ในคิว
ประโยชน์ด้าน performance ก็น่าจะน้อยด้วย object จำนวนมากถูกเข้าถึงแค่จากเธรดเดียว บาง object ถูกหลายเธรดเข้าถึง แต่ object ที่ถูกเข้าถึงแบบ exclusive จากเธรดเดียว แล้วภายหลังถูกเข้าถึงแบบ exclusive จากอีกเธรดหนึ่งนั้นพบได้น้อย
เพิ่งอ่านข่าว tranched bread ไปก่อนหน้านี้ แล้วตอนนี้ยังมีอันนี้อีกเหรอ? เป็นยุคที่น่าทึ่งจริง ๆ
ตอนโปรเจกต์ Unladen Swallow [1] ค่อย ๆ เงียบหายไปก็รู้สึกเสียดายนิดหน่อย ดีใจที่เห็น Python กลับมาอยู่บนเส้นทาง optimization หลักอีกครั้ง
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow
อยากให้ช่วยอธิบายเหมือนอธิบายให้เด็กห้าขวบฟัง
เข้าใจในเชิงแนวคิดว่า GIL คืออะไร แต่ผลกระทบของการเปลี่ยนแปลงนี้คืออะไร? ตอนนี้ package ต่าง ๆ จะพัง โดยแลกกับการคาดหวังว่า performance โดยรวมจะดีขึ้นหรือเปล่า?
แม้ไม่ใช่งาน CPU หนัก ๆ การเปลี่ยนแปลงนี้ก็ยังมีประโยชน์ได้ ช่วงหลังโค้ดจำนวนมากเขียนด้วยฟีเจอร์ภาษา asyncio แบบ native ของ Python มันทำงานในเธรดเดียวโดย yield การรันผ่าน async/await เหมือน NodeJS และใช้แค่เธรดเดียวก็ทำ throughput ได้ดีระดับหลายพัน requests ต่อวินาที
แต่ปัญหาใหญ่คือทันทีที่ทำ งาน CPU ใด ๆ มันจะบล็อก coroutine อื่นทั้งหมด ทำให้เกิดปัญหาคลุมเครือสารพัดและทำให้จำนวน requests ต่อวินาทีพัง เช่น คุณอาจเห็น I/O timeout แบบสุ่มใน coroutine หนึ่ง ทั้งที่สาเหตุจริงคือ coroutine อีกตัวหนึ่งใช้ CPU อยู่ชั่วครู่ การสังเกตให้รู้ว่าทำไมเรื่องนี้เกิดขึ้นก็ยากมาก asyncio มีฟังก์ชัน
asyncio.to_thread()[1] ที่ช่วยย้ายงานที่บล็อกออกจากเธรดหลัก แต่เพราะ GIL มันจึงไม่ได้แยกงานที่เน้น CPU ออกจริง ๆ เพื่อไม่ให้ไปรบกวน coroutine อื่น[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
สำหรับคนที่สงสัย GIL ย่อมาจาก Global Interpreter Lock
มีแหล่งข้อมูลที่สรุปภาพใหญ่กว่านี้ได้ดีไหม?
ในที่สุดก็รอดู benchmark ของเครื่องมือต่าง ๆ ได้แล้ว