ล็อคล่ามระดับโลกคืออะไรและทำไมจึงมีปัญหา?
มีเสียงดังรบกวนมากมายเกี่ยวกับการนำ GIL ออกจาก Python และฉันต้องการที่จะเข้าใจว่าทำไมจึงมีความสำคัญ ฉันไม่เคยเขียนคอมไพเลอร์หรือล่ามเองดังนั้นอย่าประหยัดกับรายละเอียดฉันอาจต้องการให้พวกเขาเข้าใจ
ล็อคล่ามระดับโลกคืออะไรและทำไมจึงมีปัญหา?
มีเสียงดังรบกวนมากมายเกี่ยวกับการนำ GIL ออกจาก Python และฉันต้องการที่จะเข้าใจว่าทำไมจึงมีความสำคัญ ฉันไม่เคยเขียนคอมไพเลอร์หรือล่ามเองดังนั้นอย่าประหยัดกับรายละเอียดฉันอาจต้องการให้พวกเขาเข้าใจ
คำตอบ:
GIL ของ Python มีจุดประสงค์เพื่อทำให้การเข้าถึงล่ามภายในเป็นไปอย่างต่อเนื่องจากหัวข้อที่แตกต่างกัน บนระบบมัลติคอร์หมายความว่าหลายเธรดไม่สามารถใช้ประโยชน์จากหลายคอร์ได้อย่างมีประสิทธิภาพ (หาก GIL ไม่ได้นำไปสู่ปัญหานี้คนส่วนใหญ่จะไม่สนใจ GIL - มันถูกยกเป็นปัญหาเนื่องจากความชุกของระบบมัลติคอร์ที่เพิ่มขึ้น) หากคุณต้องการที่จะเข้าใจในรายละเอียด คุณสามารถดูวิดีโอนี้หรือดูสไลด์ชุดนี้ มันอาจเป็นข้อมูลมากเกินไป แต่คุณก็ขอรายละเอียด :-)
โปรดทราบว่า GIL ของ Python เป็นเพียงปัญหาของ CPython ซึ่งเป็นการนำไปใช้อ้างอิงเท่านั้น Jython และ IronPython ไม่มี GIL ในฐานะนักพัฒนา Python โดยทั่วไปคุณจะไม่เจอ GIL ยกเว้นว่าคุณเขียนส่วนขยาย C ผู้เขียนส่วนขยาย C จำเป็นต้องปล่อย GIL เมื่อส่วนขยายของพวกเขาปิดกั้น I / O เพื่อให้เธรดอื่น ๆ ในกระบวนการ Python มีโอกาสทำงาน
regex
, lxml
, numpy
โมดูล Cython อนุญาตให้เผยแพร่ GIL ในรหัสที่กำหนดเองเช่นb2a_bin(data)
สมมติว่าคุณมีหลายหัวข้อที่ไม่ได้จริงๆสัมผัสข้อมูลของกันและกัน สิ่งเหล่านั้นควรดำเนินการอย่างอิสระเท่าที่จะทำได้ หากคุณมี "ล็อคส่วนกลาง" ซึ่งคุณจำเป็นต้องได้รับเพื่อที่จะเรียกใช้ฟังก์ชั่นนั้น (นั่นคือ) จะกลายเป็นคอขวด คุณสามารถไขลานไม่ได้รับประโยชน์มากนักจากการมีหลายเธรดในตอนแรก
เพื่อนำมาเปรียบเทียบในโลกแห่งความเป็นจริง: ลองนึกภาพนักพัฒนา 100 คนที่ทำงานใน บริษัท ด้วยกาแฟแก้วเดียว นักพัฒนาส่วนใหญ่จะใช้เวลารอกาแฟแทนการเข้ารหัส
สิ่งนี้ไม่เฉพาะเจาะจงกับ Python - ฉันไม่ทราบรายละเอียดว่า Python ต้องการอะไร GIL ตั้งแต่แรก อย่างไรก็ตามหวังว่ามันจะทำให้คุณมีความคิดที่ดีขึ้นเกี่ยวกับแนวคิดทั่วไป
ก่อนอื่นเรามาทำความเข้าใจกับสิ่งที่ python GIL จัดหาให้:
การดำเนินการ / คำสั่งใด ๆ จะดำเนินการในล่าม GIL เพื่อให้แน่ใจว่าล่ามที่จัดขึ้นโดยหัวข้อเดียวในทันทีโดยเฉพาะอย่างยิ่งเวลา และโปรแกรมไพ ธ อนของคุณที่มีหลายเธรดจะทำงานในล่ามตัวเดียว ในเวลาใดก็ตามล่ามนี้จะจัดขึ้นโดยเธรดเดียว มันหมายความว่าเฉพาะหัวข้อที่มีการถือครองล่ามที่มีการทำงานในทันทีเวลาใด ๆ
ตอนนี้ทำไมถึงเป็นปัญหา:
เครื่องของคุณอาจมีหลายคอร์ / โปรเซสเซอร์ และหลายคอร์อนุญาตให้หลายเธรดสามารถดำเนินการได้พร้อมกันเช่นหลายเธรดสามารถดำเนินการได้ทันที . แต่เนื่องจากล่ามถูกจัดขึ้นโดยเธรดเดี่ยวเธรดอื่นจึงไม่ทำอะไรแม้ว่าพวกเขาจะเข้าถึงคอร์ ดังนั้นคุณจะไม่ได้รับประโยชน์ใด ๆ จากหลายคอร์เนื่องจากในทันทีใด ๆ เพียงแกนเดียวซึ่งเป็นแกนที่ใช้โดยเธรดที่ถือล่ามอยู่ในขณะนี้ ดังนั้นโปรแกรมของคุณจะใช้เวลาดำเนินการนานราวกับว่ามันเป็นโปรแกรมเธรดเดี่ยว
อย่างไรก็ตามอาจมีการปิดกั้นหรือการใช้งานที่ยาวนานเช่น I / O การประมวลผลภาพและการบีบอัดตัวเลข NumPy ซึ่งเกิดขึ้นนอก GIL ที่นำมาจากที่นี่ ดังนั้นสำหรับการดำเนินการดังกล่าวการดำเนินการแบบมัลติเธรดจะยังคงเร็วกว่าการดำเนินการแบบเธรดเดียวแม้จะมี GIL อยู่ก็ตาม ดังนั้น GIL จึงไม่ใช่คอขวดเสมอไป
แก้ไข: GIL เป็นรายละเอียดการใช้งานของ CPython IronPython และ Jython ไม่มี GIL ดังนั้นโปรแกรมแบบมัลติเธรดที่แท้จริงควรเป็นไปได้ในพวกเขาคิดว่าฉันไม่เคยใช้ PyPy และ Jython และไม่แน่ใจในเรื่องนี้
Python ไม่อนุญาตให้มีหลายเธรดในความหมายที่แท้จริงของคำ มันมีแพ็คเกจแบบมัลติเธรด แต่ถ้าคุณต้องการมัลติเธรดเพื่อเพิ่มความเร็วรหัสของคุณก็มักจะไม่ควรใช้มัน Python มีโครงสร้างที่เรียกว่า Global Interpreter Lock (GIL)
https://www.youtube.com/watch?v=ph374fJqFPE
GIL ทำให้แน่ใจว่ามีเพียงหนึ่งใน 'กระทู้' ของคุณเท่านั้นที่สามารถดำเนินการได้ในคราวเดียว เธรดรับ GIL, ทำงานเล็กน้อยแล้วส่ง GIL ไปยังเธรดถัดไป สิ่งนี้เกิดขึ้นได้อย่างรวดเร็วดังนั้นในสายตามนุษย์มันอาจดูเหมือนว่าเธรดของคุณกำลังดำเนินการแบบขนาน แต่จริงๆแล้วพวกเขาเพียงแค่หันมาใช้ซีพียูแกนเดียวกัน การผ่าน GIL ทั้งหมดนี้เพิ่มค่าใช้จ่ายในการดำเนินการ ซึ่งหมายความว่าหากคุณต้องการให้โค้ดของคุณทำงานเร็วขึ้นการใช้เธรดแพ็คเกจมักจะไม่ใช่ความคิดที่ดี
มีเหตุผลที่จะใช้ชุดเธรดของ Python หากคุณต้องการดำเนินการบางอย่างพร้อมกันและประสิทธิภาพไม่ได้เป็นเรื่องที่กังวลเลยมันก็ดีและสะดวกมาก หรือถ้าคุณกำลังเรียกใช้รหัสที่ต้องรอบางสิ่ง (เช่นบาง IO) มันอาจสมเหตุสมผลดี แต่ไลบรารีเธรดจะไม่ยอมให้คุณใช้คอร์ CPU เพิ่มเติม
มัลติเธรดสามารถเอาต์ซอร์ซไปยังระบบปฏิบัติการ (โดยทำการประมวลผลหลายอย่าง), แอปพลิเคชั่นภายนอกบางตัวที่เรียกรหัส Python ของคุณ (เช่น Spark หรือ Hadoop) หรือรหัสบางอย่างที่รหัส Python ของคุณโทรออก (เช่น: รหัสเรียกฟังก์ชั่น C ที่ทำสิ่งที่มีหลายเธรดราคาแพง)
เมื่อใดก็ตามที่สองกระทู้มีการเข้าถึงตัวแปรเดียวกันคุณมีปัญหา ใน C ++ ตัวอย่างเช่นวิธีการหลีกเลี่ยงปัญหาคือการกำหนดการล็อก mutex บางอย่างเพื่อป้องกันไม่ให้เธรดสองเธรดใส่ตัวตั้งค่าของวัตถุในเวลาเดียวกัน
การมัลติเธรดเป็นไปได้ในไพ ธ อน แต่เธรดสองตัวไม่สามารถดำเนินการได้ในเวลาเดียวกันที่ความละเอียดปลีกย่อยมากกว่าคำสั่งไพ ธ อนอย่างเดียว เธรดที่กำลังทำงานอยู่ได้รับการล็อคระดับโลกชื่อ GIL
ซึ่งหมายความว่าถ้าคุณเริ่มเขียนโค้ดแบบมัลติเธรดเพื่อใช้ประโยชน์จากตัวประมวลผลแบบมัลติคอร์ประสิทธิภาพของคุณจะไม่ดีขึ้น วิธีแก้ปัญหาตามปกติประกอบด้วยการประมวลผลหลายขั้นตอน
โปรดทราบว่ามันเป็นไปได้ที่จะปล่อย GIL ถ้าคุณอยู่ในวิธีที่คุณเขียนใน C เช่น
การใช้ GIL นั้นไม่ได้มีมา แต่กำเนิดจาก Python แต่มีสำหรับล่ามบางตัวรวมถึง CPython ที่พบบ่อยที่สุด (#edited ดูความคิดเห็น)
ปัญหา GIL ยังคงใช้ได้ใน Python 3000
เอกสาร Python 3.7
ฉันอยากจะเน้นคำพูดต่อไปนี้จากเอกสารPythonthreading
:
รายละเอียดการติดตั้ง CPython: ใน CPython เนื่องจาก Global Interpreter Lock มีเพียงเธรดเดียวเท่านั้นที่สามารถเรียกใช้งานโค้ด Python ได้ในคราวเดียว (แม้ว่าไลบรารี่ที่มุ่งเน้นประสิทธิภาพบางอย่างอาจเอาชนะข้อ จำกัด นี้ได้) หากคุณต้องการใช้งานของคุณที่จะทำให้การใช้งานที่ดีขึ้นของทรัพยากรคอมพิวเตอร์เครื่องแบบ multi-core คุณจะได้รับคำแนะนำในการใช้งานหรือ
multiprocessing
concurrent.futures.ProcessPoolExecutor
อย่างไรก็ตามการทำเกลียวยังคงเป็นรูปแบบที่เหมาะสมหากคุณต้องการใช้งาน I / O ที่เชื่อมโยงหลาย ๆ งานพร้อมกัน
ลิงก์นี้ไปยังรายการอภิธานศัพท์global interpreter lock
ซึ่งอธิบายว่า GIL บอกเป็นนัยว่าการขนานของเธรดใน Python นั้นไม่เหมาะสมสำหรับงานที่เชื่อมโยงกับ CPU :
กลไกที่ใช้โดยล่าม CPython เพื่อให้มั่นใจว่ามีเพียงหนึ่งเธรดที่เรียกใช้งาน Python bytecode ในแต่ละครั้ง สิ่งนี้จะช่วยให้การติดตั้ง CPython ง่ายขึ้นโดยการสร้างโมเดลวัตถุ (รวมถึงประเภทที่มีความสำคัญในตัวเช่น dict) โดยปริยายปลอดภัยต่อการเข้าถึงพร้อมกัน การล็อคล่ามทั้งหมดทำให้ล่ามเป็นแบบมัลติเธรดได้ง่ายขึ้นโดยเสียค่าใช้จ่ายในการขนานที่จ่ายได้จากเครื่องมัลติโปรเซสเซอร์
อย่างไรก็ตามโมดูลส่วนขยายบางส่วนไม่ว่าจะเป็นแบบมาตรฐานหรือของบุคคลที่สามได้รับการออกแบบมาเพื่อเผยแพร่ GIL เมื่อทำงานที่ต้องใช้การคำนวณสูงเช่นการบีบอัดหรือการบีบอัดข้อมูล นอกจากนี้ GIL จะถูกปล่อยออกมาเสมอเมื่อทำ I / O
ความพยายามที่ผ่านมาในการสร้างล่าม "ฟรีเธรด" (ที่ล็อคข้อมูลที่ใช้ร่วมกันในรายละเอียดปลีกย่อยมาก) ไม่ประสบความสำเร็จเพราะประสิทธิภาพในกรณีทั่วไปโปรเซสเซอร์เดียว เป็นที่เชื่อกันว่าการเอาชนะปัญหาเรื่องประสิทธิภาพนี้จะทำให้การใช้งานมีความซับซ้อนมากขึ้นและมีค่าใช้จ่ายสูงในการบำรุงรักษา
คำพูดนี้ยังบอกเป็นนัยว่า dicts และการกำหนดตัวแปรจึงปลอดภัยต่อการใช้งานเช่นเดียวกับรายละเอียดการใช้งานของ CPython:
ถัดไปเอกสารสำหรับmultiprocessing
แพคเกจอธิบายวิธีเอาชนะ GIL ด้วยกระบวนการวางไข่ในขณะที่เปิดเผยส่วนต่อประสานที่คล้ายกับของthreading
:
มัลติโปรเซสเซอร์เป็นแพ็คเกจที่สนับสนุนกระบวนการวางไข่โดยใช้ API ที่คล้ายกับโมดูลเธรด แพคเกจมัลติโปรเซสเซอร์นำเสนอทั้งการทำงานพร้อมกันในท้องถิ่นและระยะไกลจัดการล็อค Interpreter ทั่วโลกอย่างมีประสิทธิภาพโดยใช้กระบวนการย่อยแทนเธรด ด้วยเหตุนี้โมดูลมัลติโปรเซสเซอร์จะทำให้โปรแกรมเมอร์สามารถใช้ประโยชน์จากโปรเซสเซอร์หลายตัวในเครื่องที่กำหนดได้อย่างเต็มที่ มันทำงานได้ทั้ง Unix และ Windows
และเอกสารสำหรับconcurrent.futures.ProcessPoolExecutor
อธิบายว่ามันใช้multiprocessing
เป็นแบ็กเอนด์:
คลาส ProcessPoolExecutor เป็นคลาสย่อย Executor ที่ใช้พูลของกระบวนการเพื่อดำเนินการเรียกแบบอะซิงโครนัส ProcessPoolExecutor ใช้โมดูลมัลติโพรเซสซึ่งอนุญาตให้ทำการล็อก Interpreter Global แบบด้านข้าง แต่ยังหมายความว่าสามารถเรียกใช้และส่งคืนออบเจ็กต์วัตถุที่เลือกได้เท่านั้น
ซึ่งควรเปรียบเทียบกับคลาสฐานอื่นThreadPoolExecutor
ที่ใช้เธรดแทนกระบวนการ
ThreadPoolExecutor เป็นคลาสย่อย Executor ที่ใช้พูลของเธรดเพื่อดำเนินการเรียกแบบอะซิงโครนัส
ซึ่งเราสรุปได้ว่าThreadPoolExecutor
เหมาะสำหรับงานที่ถูกผูกไว้ I / O เท่านั้นในขณะที่ProcessPoolExecutor
ยังสามารถจัดการงานที่ผูกกับ CPU ได้
คำถามต่อไปนี้ถามว่าทำไม GIL ถึงมีอยู่ในสถานที่แรก: ทำไมต้องล่ามแปลภาษาสากล?
การทดลองเทียบกับกระบวนการ
ที่Multiprocessing กับ Threading Pythonฉันได้ทำการวิเคราะห์การทดลองของโพรเซสกับเธรดใน Python
ดูตัวอย่างผลลัพธ์อย่างรวดเร็ว:
เหตุใด Python (CPython และอื่น ๆ ) จึงใช้ GIL
จากhttp://wiki.python.org/moin/GlobalInterpreterLock
ใน CPython การล็อคตัวแปลภาษาทั่วโลกหรือ GIL เป็น mutex ที่ป้องกันเธรดหลายเธรดจากการเรียกใช้งาน Python bytecode ในครั้งเดียว การล็อกนี้เป็นสิ่งจำเป็นเนื่องจากการจัดการหน่วยความจำของ CPython ไม่ปลอดภัยสำหรับเธรด
จะลบออกจาก Python ได้อย่างไร
เช่นเดียวกับ Lua บางที Python อาจเริ่ม VM หลายตัว แต่หลามไม่ทำอย่างนั้นฉันเดาว่าควรมีเหตุผลอื่น ๆ
ใน Numpy หรืออื่น ๆ ไพ ธ อนไลบรารี่บางครั้งการปล่อย GIL ไปยังเธรดอื่นสามารถเพิ่มประสิทธิภาพของโปรแกรมทั้งหมด
ฉันต้องการแบ่งปันตัวอย่างจากหนังสือมัลติเธรดสำหรับ Visual Effects ดังนั้นนี่คือสถานการณ์การล็อกแบบคลาสสิก
static void MyCallback(const Context &context){
Auto<Lock> lock(GetMyMutexFromContext(context));
...
EvalMyPythonString(str); //A function that takes the GIL
...
}
ตอนนี้ให้พิจารณาเหตุการณ์ในลำดับที่ทำให้เกิดการล็อคตาย
╔═══╦════════════════════════════════════════╦══════════════════════════════════════╗
║ ║ Main Thread ║ Other Thread ║
╠═══╬════════════════════════════════════════╬══════════════════════════════════════╣
║ 1 ║ Python Command acquires GIL ║ Work started ║
║ 2 ║ Computation requested ║ MyCallback runs and acquires MyMutex ║
║ 3 ║ ║ MyCallback now waits for GIL ║
║ 4 ║ MyCallback runs and waits for MyMutex ║ waiting for GIL ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝