มัลติเธรดใน Python พร้อมตัวอย่าง: เรียนรู้ GIL ใน Python
⚡ สรุปอย่างชาญฉลาด
มัลติเธรดใน Python การใช้มัลติเธรดจะทำงานภายในกระบวนการเดียว ทำให้เธรดเหล่านั้นใช้หน่วยความจำร่วมกันและทำงานพร้อมกันได้ โมดูลการสร้างเธรดจะสร้างและจัดการเธรดเหล่านี้ ในขณะที่ Global Interpreter Lock จะจำกัดการทำงานแบบขนานอย่างแท้จริง ทำให้เทคนิคนี้เหมาะที่สุดสำหรับงานที่เน้นการรับและส่งข้อมูล

การขอ Python ภาษาโปรแกรมอนุญาตให้คุณใช้การประมวลผลแบบหลายโปรเซสหรือมัลติเธรดดิ้ง ในบทเรียนนี้ คุณจะได้เรียนรู้วิธีการเขียนแอปพลิเคชันแบบมัลติเธรดดิ้งในภาษาโปรแกรม Python.
กระทู้คืออะไร?
เธรด (Thread) คือหน่วยการทำงานในการเขียนโปรแกรมแบบขนาน (Concurrent Programming) มัลติเธรดดิ้ง (Multithreading) เป็นเทคนิคที่ช่วยให้ซีพียูสามารถประมวลผลงานหลายอย่างของกระบวนการเดียวได้พร้อมกัน เธรดเหล่านี้สามารถทำงานแยกกันได้ในขณะที่แบ่งปันทรัพยากรของกระบวนการ
กระบวนการคืออะไร?
กระบวนการทำงานโดยพื้นฐานแล้วคือโปรแกรมที่กำลังทำงานอยู่ เมื่อคุณเริ่มใช้งานแอปพลิเคชันบนคอมพิวเตอร์ของคุณ (เช่น เว็บเบราว์เซอร์หรือโปรแกรมแก้ไขข้อความ) ระบบปฏิบัติการจะสร้างกระบวนการทำงานขึ้นมา กระบวนการ
มัลติเธรดคืออะไร Python?
มัลติเธรดใน Python การเขียนโปรแกรมแบบมัลติเธรดเป็นเทคนิคที่รู้จักกันดี ซึ่งเธรดหลายตัวในกระบวนการเดียวกันจะแบ่งปันพื้นที่ข้อมูลกับเธรดหลัก ทำให้การแบ่งปันข้อมูลและการสื่อสารระหว่างเธรดง่ายและมีประสิทธิภาพ เธรดมีน้ำหนักเบากว่ากระบวนการ เธรดหลายตัวสามารถทำงานแยกกันได้ในขณะที่แบ่งปันทรัพยากรของกระบวนการ จุดประสงค์ของการเขียนโปรแกรมแบบมัลติเธรดคือการเรียกใช้งานหลายงานและหลายฟังก์ชันพร้อมกัน
มัลติโปรเซสเซอร์คืออะไร?
การประมวลผลหลายขั้นตอน ช่วยให้คุณสามารถรันกระบวนการที่ไม่เกี่ยวข้องหลายกระบวนการพร้อมกันได้ กระบวนการเหล่านี้ไม่แบ่งปันทรัพยากรและสื่อสารกันผ่าน IPC
Python มัลติเธรดกับมัลติโปรเซสเซอร์
เพื่อให้เข้าใจกระบวนการและเธรด ลองพิจารณาสถานการณ์นี้: ไฟล์ .exe ในคอมพิวเตอร์ของคุณคือโปรแกรม เมื่อคุณเปิดมัน ระบบปฏิบัติการจะโหลดมันเข้าสู่หน่วยความจำ และซีพียูจะประมวลผลมัน อินสแตนซ์ของโปรแกรมที่กำลังทำงานอยู่นั้นเรียกว่ากระบวนการ
ทุกกระบวนการมีองค์ประกอบพื้นฐานสองอย่าง:
- การขอ Code
- ข้อมูล
ตอนนี้กระบวนการสามารถมีส่วนย่อยหนึ่งหรือหลายส่วนที่เรียกว่า หัวข้อ ขึ้นอยู่กับสถาปัตยกรรมของระบบปฏิบัติการ คุณอาจมองว่าเธรดเป็นส่วนหนึ่งของกระบวนการที่ระบบปฏิบัติการสามารถเรียกใช้งานแยกต่างหากได้
กล่าวอีกนัยหนึ่งคือ มันคือชุดคำสั่งที่ระบบปฏิบัติการสามารถประมวลผลได้อย่างอิสระ เธรดภายในกระบวนการเดียวจะใช้ข้อมูลร่วมกันของกระบวนการนั้น และถูกออกแบบมาให้ทำงานร่วมกันเพื่ออำนวยความสะดวกในการประมวลผลแบบขนาน
เหตุใดจึงต้องใช้มัลติเธรด?
มัลติเธรดช่วยให้คุณแบ่งแอปพลิเคชันออกเป็นหลายงานย่อยและรันงานเหล่านี้พร้อมกันได้ หากคุณใช้มัลติเธรดอย่างเหมาะสม ความเร็ว ประสิทธิภาพ และการเรนเดอร์ของแอปพลิเคชันของคุณก็จะดีขึ้น
Python มัลติเธรด
Python รองรับโครงสร้างทั้งสำหรับการประมวลผลแบบหลายโปรเซสและการทำงานแบบมัลติเธรด ในบทเรียนนี้ คุณจะเน้นไปที่การใช้งานเป็นหลัก มัลติเธรด แอพพลิเคชั่นด้วย Pythonมีโมดูลหลักสองโมดูลที่สามารถใช้จัดการเธรดได้ Python:
- การขอ ด้าย โมดูลและ
- การขอ เกลียว โมดูล
อย่างไรก็ตามใน Pythonนอกจากนี้ยังมีสิ่งที่เรียกว่า Global Interpreter Lock (GIL) ซึ่งไม่ได้ช่วยเพิ่มประสิทธิภาพมากนัก และอาจทำให้ประสิทธิภาพลดลงด้วยซ้ำ ลด ประสิทธิภาพของแอพพลิเคชั่นแบบมัลติเธรดบางตัว คุณจะได้เรียนรู้ทั้งหมดเกี่ยวกับเรื่องนี้ในส่วนต่อๆ ไปของบทช่วยสอนนี้
โมดูลเธรดและเธรด
สองโมดูลที่คุณจะได้เรียนรู้ในบทช่วยสอนนี้คือ โมดูลเธรด และ โมดูลเกลียว.
อย่างไรก็ตาม โมดูลเธรดเลิกใช้มานานแล้ว เริ่มด้วย Python 3 ได้รับการกำหนดให้ล้าสมัยและเข้าถึงได้เฉพาะในชื่อเท่านั้น _ด้าย สำหรับความเข้ากันได้ย้อนหลัง
คุณควรใช้ระดับที่สูงกว่า เกลียว โมดูลสำหรับแอปพลิเคชันที่คุณตั้งใจจะนำไปใช้งาน โมดูลเธรดได้กล่าวถึงไว้ในที่นี้เพื่อจุดประสงค์ทางการศึกษาเท่านั้น
โมดูลเธรด
ไวยากรณ์ในการสร้างเธรดใหม่โดยใช้โมดูลนี้เป็นดังนี้:
thread.start_new_thread(function_name, arguments)
เอาล่ะ คุณได้พูดถึงทฤษฎีพื้นฐานในการเริ่มเขียนโค้ดแล้ว ดังนั้นเปิดของคุณ IDLE หรือสมุดบันทึกและพิมพ์ดังต่อไปนี้:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
บันทึกไฟล์และกด F5 เพื่อรันโปรแกรม หากทุกอย่างถูกต้อง นี่คือผลลัพธ์ที่คุณควรเห็น:
คุณจะได้เรียนรู้เพิ่มเติมเกี่ยวกับสภาวะการแข่งขันและวิธีการรับมือกับสภาวะเหล่านั้นในส่วนถัดไป
คำอธิบายรหัส
- คำสั่งเหล่านี้นำเข้าโมดูลเวลาและเธรด ซึ่งใช้ในการจัดการการทำงานและการหน่วงเวลาของโปรแกรม Python หัวข้อ
- ที่นี่คุณได้กำหนดฟังก์ชันที่เรียกว่า เธรด_ทดสอบ, ซึ่งจะถูกเรียกโดย start_new_thread เมธอดนี้จะทำงานในลูป while เป็นเวลาสี่รอบ และพิมพ์ชื่อของเธรดที่เรียกใช้ เมื่อการทำงานเสร็จสิ้น จะแสดงข้อความว่าเธรดนั้นทำงานเสร็จแล้ว
- นี่คือส่วนหลักของโปรแกรมของคุณ ที่นี่คุณเพียงแค่โทรหา start_new_thread วิธีการกับ thread_test ฟังก์ชันเป็นอาร์กิวเมนต์ การทำเช่นนี้จะสร้างเธรดใหม่สำหรับฟังก์ชันที่คุณส่งเป็นอาร์กิวเมนต์และเริ่มทำงาน โปรดทราบว่าคุณสามารถแทนที่ (thread_test) ด้วยฟังก์ชันอื่นใดที่คุณต้องการเรียกใช้ในเธรดได้
โมดูลการทำเกลียว
โมดูลนี้เป็นการนำเอาการทำงานแบบมัลติเธรดมาใช้ในระดับสูงในภาษาโปรแกรม Python และเป็นมาตรฐานที่ใช้กันอย่างแพร่หลายในการจัดการแอปพลิเคชันแบบมัลติเธรด โดยมีคุณสมบัติหลากหลายกว่าโมดูลเธรด
โครงสร้างของโมดูลการทำเกลียว
นี่คือรายการฟังก์ชันที่มีประโยชน์บางส่วนที่กำหนดไว้ในโมดูลนี้:
| ชื่อฟังก์ชั่น | Descriptไอออน |
|---|---|
| ใช้งานนับ() | ส่งคืนค่าการนับของ ด้าย สิ่งของที่ยังมีชีวิตอยู่ |
| ปัจจุบันกระทู้() | ส่งกลับวัตถุปัจจุบันของคลาสเธรด |
| การระบุ () | แสดงรายการวัตถุ Thread ที่ใช้งานอยู่ทั้งหมด |
| isDaemon() | คืนค่าเป็นจริงหากเธรดเป็น daemon |
| ยังมีชีวิตอยู่() | คืนค่าเป็นจริงหากเธรดยังมีชีวิตอยู่ |
| วิธีการคลาสเธรด | |
| เริ่ม () | เริ่มต้นกิจกรรมของเธรด จะต้องถูกเรียกเพียงครั้งเดียวสำหรับแต่ละเธรด เนื่องจากจะทำให้เกิดข้อผิดพลาดรันไทม์หากถูกเรียกหลายครั้ง |
| วิ่ง() | เมธอดนี้แสดงถึงกิจกรรมของเธรดและสามารถแทนที่ได้โดยคลาสที่ขยายคลาสเธรด |
| เข้าร่วม () | มันจะบล็อกการดำเนินการของโค้ดอื่น ๆ จนกระทั่งเธรดที่เมธอด join() ถูกเรียกถูกยกเลิก |
เรื่องราวเบื้องหลัง: คลาสเธรด
ก่อนที่คุณจะเริ่มเขียนโปรแกรมแบบมัลติเธรดโดยใช้โมดูล threading คุณจำเป็นต้องเข้าใจคลาส Thread ก่อน คลาส Thread เป็นคลาสหลักที่กำหนดเทมเพลตและการทำงานของเธรด Python.
วิธีที่พบได้บ่อยที่สุดในการสร้างมัลติเธรด Python แอปพลิเคชันนี้คือการประกาศคลาสที่สืบทอดมาจากคลาส Thread และเขียนทับเมธอด run() ของคลาส Thread นั้น
โดยสรุปคลาส Thread หมายถึงลำดับโค้ดที่ทำงานแยกกัน ด้าย ของการควบคุม
ดังนั้นเมื่อเขียนแอปแบบมัลติเธรด คุณจะต้องทำสิ่งต่อไปนี้:
- กำหนดคลาสที่สืบทอดมาจากคลาส Thread
- แทนที่ __ในนั้น__ นวกรรมิก
- แทนที่ วิ่ง() วิธี
เมื่อสร้างวัตถุเธรดแล้ว เริ่ม () สามารถใช้วิธีนี้ในการเริ่มต้นการดำเนินการกิจกรรมนี้ และ เข้าร่วม () สามารถใช้เมธอดนี้เพื่อบล็อกโค้ดอื่นๆ ทั้งหมดได้จนกว่ากิจกรรมปัจจุบันจะเสร็จสิ้น
ทีนี้ เรามาลองใช้โมดูล threading เพื่อนำตัวอย่างก่อนหน้านี้ไปใช้กันดู เริ่มการทำงานอีกครั้งได้เลย IDLE และพิมพ์ตามนี้:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
นี่จะเป็นผลลัพธ์เมื่อคุณรันโค้ดด้านบน:
คำอธิบายรหัส
- ส่วนนี้เหมือนกับตัวอย่างก่อนหน้านี้ ตรงนี้ คุณจะนำเข้าโมดูลเวลาและเธรด ซึ่งใช้ในการจัดการการทำงานและการหน่วงเวลา Python หัวข้อ
- ในบิตนี้ คุณกำลังสร้างคลาสที่เรียกว่า threadtester ซึ่งจะสืบทอดหรือขยาย ด้าย คลาสของโมดูล threading นี่เป็นหนึ่งในวิธีที่พบบ่อยที่สุดในการสร้างเธรดใน Pythonอย่างไรก็ตาม คุณควรเขียนทับเฉพาะคอนสตรัคเตอร์และเท่านั้น วิ่ง() วิธีการในแอปของคุณ ดังที่คุณเห็นในตัวอย่างโค้ดข้างต้น __ในนั้น__ วิธีการ (ตัวสร้าง) ได้รับการแทนที่แล้ว ในทำนองเดียวกัน คุณได้แทนที่ด้วย วิ่ง() วิธี. ประกอบด้วยโค้ดที่คุณต้องการดำเนินการภายในเธรด ในตัวอย่างนี้ คุณได้เรียกใช้ฟังก์ชัน thread_test()
- นี่คือเมธอด thread_test() ซึ่งรับค่าของ i โดยรับค่า i เป็นอาร์กิวเมนต์ จะลดค่าลง 1 ในแต่ละรอบการวนซ้ำ และวนลูปผ่านส่วนที่เหลือของโค้ดจนกว่า i จะเป็น 0 ในแต่ละรอบการวนซ้ำ จะพิมพ์ชื่อของเธรดที่กำลังทำงานอยู่ และหยุดการทำงานเป็นเวลา wait วินาที (ซึ่งรับเป็นอาร์กิวเมนต์เช่นกัน)
- thread1 = threadtester(1, “First Thread”, 1) ที่นี่ เรากำลังสร้างเธรดและส่งผ่านพารามิเตอร์ทั้งสามที่เราประกาศใน __init__ พารามิเตอร์แรกคือ id ของเธรด พารามิเตอร์ที่สองคือชื่อของเธรด และพารามิเตอร์ที่สามคือตัวนับ ซึ่งจะกำหนดจำนวนครั้งที่ลูป while ควรรัน
- thread2.start() เมธอด start ใช้สำหรับเริ่มการทำงานของเธรด ภายในแล้ว ฟังก์ชัน start() จะเรียกเมธอด run() ของคลาสของคุณ
- thread3.join() วิธีการ join() บล็อกการทำงานของโค้ดอื่น ๆ และรอจนกว่าเธรดที่ถูกเรียกว่าเสร็จสิ้น
อย่างที่คุณทราบแล้ว เธรดที่อยู่ในกระบวนการเดียวกันจะสามารถเข้าถึงหน่วยความจำและข้อมูลของกระบวนการนั้นได้ ดังนั้น หากมีเธรดมากกว่าหนึ่งเธรดพยายามเปลี่ยนแปลงหรือเข้าถึงข้อมูลพร้อมกัน อาจทำให้เกิดข้อผิดพลาดได้
ในส่วนถัดไป คุณจะได้เห็นปัญหาต่างๆ ที่อาจเกิดขึ้นเมื่อเธรดเข้าถึงข้อมูลและส่วนวิกฤตโดยไม่ตรวจสอบธุรกรรมการเข้าถึงที่มีอยู่ก่อน
เดดล็อคและสภาวะการแข่งขัน
ก่อนที่จะเรียนรู้เกี่ยวกับภาวะติดตาย (deadlock) และภาวะการแข่งขัน (race condition) จะเป็นประโยชน์อย่างยิ่งหากเราเข้าใจคำจำกัดความพื้นฐานบางประการที่เกี่ยวข้องกับการเขียนโปรแกรมแบบขนาน:
- ส่วนสำคัญ: นี่คือส่วนของโค้ดที่เข้าถึงหรือแก้ไขตัวแปรที่ใช้ร่วมกัน และต้องดำเนินการในรูปแบบธุรกรรมอะตอมิกเท่านั้น
- การสลับบริบท: เป็นกระบวนการที่ซีพียูใช้ในการจัดเก็บสถานะของเธรดก่อนที่จะเปลี่ยนจากงานหนึ่งไปอีกงานหนึ่ง เพื่อให้สามารถกลับมาทำงานต่อจากจุดเดิมได้ในภายหลัง
การหยุดชะงัก
การหยุดชะงัก เป็นปัญหาที่นักพัฒนาซอฟต์แวร์กลัวมากที่สุดเมื่อเขียนแอปพลิเคชันแบบขนาน/มัลติเธรด Pythonวิธีที่ดีที่สุดในการทำความเข้าใจภาวะติดตาย (deadlock) คือการใช้ตัวอย่างปัญหาคลาสสิกในวิทยาการคอมพิวเตอร์ที่รู้จักกันในชื่อ... ห้องอาหาร Philoปัญหาโซเฟอร์
คำชี้แจงปัญหาสำหรับนักปรัชญาการรับประทานอาหารมีดังนี้:
ดังแสดงในแผนภาพ นักปรัชญาห้าคนนั่งอยู่ที่โต๊ะกลม โดยมีจานสปาเก็ตตี้ (พาสต้าชนิดหนึ่ง) ห้าจานและส้อมห้าอันวางอยู่บนโต๊ะ
ห้องอาหาร Philoปัญหาโซเฟอร์
ในช่วงเวลาใดก็ตาม นักปรัชญาจะต้องกินหรือกำลังคิด
นอกจากนี้ นักปรัชญาต้องหยิบส้อมสองอันที่อยู่ติดกับตัวเขา (นั่นคือส้อมซ้ายและขวา) ก่อนจึงจะกินสปาเก็ตตี้ได้ ปัญหาทางตันเกิดขึ้นเมื่อนักปรัชญาทั้งห้าคนหยิบส้อมขวาพร้อมกัน
เนื่องจากนักปรัชญาแต่ละคนมีส้อมคนละหนึ่งอัน พวกเขาจึงต้องรอให้คนอื่นวางส้อมลง ผลก็คือไม่มีใครกินสปาเก็ตตี้ได้
ในทำนองเดียวกัน ในระบบที่ทำงานพร้อมกัน จะเกิดการหยุดชะงักเมื่อเธรดหรือกระบวนการต่างๆ (นักปรัชญา) พยายามจะรับทรัพยากรระบบที่ใช้ร่วมกัน (forks) ในเวลาเดียวกัน ส่งผลให้กระบวนการต่างๆ ไม่มีโอกาสดำเนินการ เนื่องจากกำลังรอทรัพยากรอื่นที่กระบวนการอื่นถืออยู่
เงื่อนไขการแข่งขัน
สภาวะการแข่งขัน (Race condition) คือสถานะที่ไม่พึงประสงค์ของโปรแกรม ซึ่งเกิดขึ้นเมื่อระบบดำเนินการสองอย่างขึ้นไปพร้อมกัน ตัวอย่างเช่น พิจารณาลูป for อย่างง่ายนี้:
i=0; # a global variable for x in range(100): print(i) i+=1;
ถ้าคุณสร้าง n เนื่องจากมีเธรดจำนวนมากที่รันโค้ดนี้พร้อมกัน คุณจึงไม่สามารถระบุค่าของ i (ซึ่งเป็นค่าที่ใช้ร่วมกันโดยเธรดต่างๆ) เมื่อโปรแกรมทำงานเสร็จสิ้นได้ เพราะในสภาพแวดล้อมมัลติเธรดจริง เธรดต่างๆ อาจทำงานซ้อนทับกัน และค่าของ i ที่ถูกเรียกใช้และแก้ไขโดยเธรดหนึ่งอาจเปลี่ยนแปลงไปในระหว่างที่เธรดอื่นเข้าถึงค่าดังกล่าว
นี่คือปัญหาหลักสองประเภทที่อาจเกิดขึ้นในระบบมัลติเธรดหรือระบบกระจาย Python ในส่วนถัดไป คุณจะได้เรียนรู้วิธีแก้ไขปัญหานี้โดยการซิงโครไนซ์เธรด
Syncหัวข้อการขัดสี
เพื่อจัดการกับเงื่อนไขการแข่งขัน เดดล็อก และปัญหาอื่นๆ ที่เกี่ยวข้องกับเธรด โมดูลเธรดจะจัดเตรียม ล็อค วัตถุ แนวคิดคือเมื่อเธรดต้องการเข้าถึงทรัพยากรเฉพาะ เธรดนั้นจะต้องล็อกทรัพยากรนั้น เมื่อเธรดล็อกทรัพยากรเฉพาะแล้ว เธรดอื่นจะไม่สามารถเข้าถึงทรัพยากรนั้นได้จนกว่าจะปลดล็อก ดังนั้น การเปลี่ยนแปลงทรัพยากรจะเป็นแบบอะตอมมิก และจะหลีกเลี่ยงเงื่อนไขการแข่งขัน
ล็อคคือการซิงโครไนซ์ระดับต่ำแบบดั้งเดิมที่ใช้งานโดย _ด้าย โมดูล ในเวลาใดเวลาหนึ่ง ตัวล็อกสามารถอยู่ในสถานะใดสถานะหนึ่งจากสองสถานะได้: ล็อค or ปลดล็อค รองรับสองวิธี:
- ได้รับ(): เมื่อสถานะการล็อกถูกปลดล็อก การเรียกเมธอด acquire() จะเปลี่ยนสถานะเป็นล็อกและส่งคืนค่า อย่างไรก็ตาม หากสถานะถูกล็อก การเรียก acquire() จะถูกบล็อกจนกว่าเมธอด release() จะถูกเรียกโดยเธรดอื่น
- ปล่อย(): release() ใช้เพื่อตั้งค่าสถานะให้ปลดล็อค เช่น ปลดล็อค สามารถเรียกได้โดยเธรดใดๆ ไม่จำเป็นต้องเป็นเธรดที่ได้รับการล็อค
นี่คือตัวอย่างการใช้ล็อกในแอปของคุณ ลองเปิดแอปของคุณดู IDLE และพิมพ์ดังต่อไปนี้:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
ตอนนี้กด F5 คุณควรเห็นผลลัพธ์ดังนี้:
คำอธิบายรหัส
- ที่นี่ คุณเพียงแค่สร้างล็อคใหม่โดยการโทรไปที่ เกลียวล็อค() ฟังก์ชั่นโรงงาน ภายใน Lock() ส่งคืนอินสแตนซ์ของคลาส Lock ที่เป็นรูปธรรมที่มีประสิทธิภาพสูงสุดซึ่งดูแลโดยแพลตฟอร์ม
- ในคำสั่งแรก คุณจะได้รับล็อคโดยการเรียกเมธอด Acquis() เมื่ออนุญาตให้ล็อคแล้ว คุณจะพิมพ์ “ได้รับล็อคแล้ว” ไปที่คอนโซล เมื่อโค้ดทั้งหมดที่คุณต้องการให้เธรดทำงานเสร็จสิ้นแล้ว คุณสามารถปลดล็อคโดยการเรียกเมธอด release()
ทฤษฎีนั้นถูกต้อง แต่คุณจะรู้ได้อย่างไรว่าการล็อกนั้นได้ผลจริง ๆ? ถ้าคุณดูที่ผลลัพธ์ คุณจะเห็นว่าคำสั่ง print แต่ละคำสั่งจะพิมพ์ออกมาทีละบรรทัดเท่านั้น จำได้ไหมว่าในตัวอย่างก่อนหน้านี้ ผลลัพธ์จาก print นั้นกระจัดกระจายเพราะมีหลายเธรดเข้าถึงเมธอด print() พร้อมกัน แต่ในที่นี้ ฟังก์ชัน print จะถูกเรียกใช้หลังจากที่ได้ล็อกแล้วเท่านั้น ดังนั้นผลลัพธ์จึงแสดงออกมาทีละบรรทัด
นอกเหนือจากกุญแจแล้ว Python นอกจากนี้ยังรองรับกลไกอื่นๆ ในการจัดการการซิงโครไนซ์เธรด ดังที่ระบุไว้ด้านล่าง:
- อาร์ล็อคส์
- Semaphores
- เงื่อนไขเพิ่มเติมสำหรับการขอคืนเงิน (Refund Policy)
- เหตุการณ์และ
- ปัญหาและอุปสรรคที่
Global Interpreter Lock (และวิธีจัดการกับมัน)
ก่อนที่จะลงรายละเอียด Pythonเพื่อนๆ ใน GIL เรามานิยามคำศัพท์บางคำที่จะเป็นประโยชน์ในการทำความเข้าใจหัวข้อต่อไปกัน:
- โค้ดที่ประมวลผลโดย CPU: หมายถึงโค้ดใดๆ ก็ตามที่จะถูกประมวลผลโดยตรงโดย CPU
- โค้ดที่เน้นการรับส่งข้อมูล (I/O-bound code): นี่คือโค้ดใดๆ ก็ตามที่เข้าถึงระบบไฟล์ผ่านทางระบบปฏิบัติการ
- CPython: มันเป็นข้อมูลอ้างอิง การดำเนินงาน of Python และสามารถอธิบายได้ว่าเป็นล่ามที่เขียนด้วยภาษาซีและ Python (ภาษาโปรแกรม).
GIL คืออะไร Python?
ล็อคล่ามสากล (GIL) in Python GIL (Global Interpreter Lock) คือตัวล็อกกระบวนการหรือมิวเท็กซ์ที่ใช้ในการจัดการกับกระบวนการต่างๆ มันช่วยให้มั่นใจได้ว่าเธรดหนึ่งๆ เท่านั้นที่จะสามารถเข้าถึงทรัพยากรใดทรัพยากรหนึ่งได้ในแต่ละครั้ง และยังป้องกันการใช้งานอ็อบเจ็กต์และไบต์โค้ดพร้อมกัน ซึ่งเป็นประโยชน์ต่อโปรแกรมแบบเธรดเดียวด้วยประสิทธิภาพที่เพิ่มขึ้น GIL ใน Python มันง่ายมากและนำไปใช้งานได้ง่าย
สามารถใช้การล็อกเพื่อให้แน่ใจว่ามีเธรดเดียวเท่านั้นที่สามารถเข้าถึงทรัพยากรเฉพาะในเวลาที่กำหนด
คุณสมบัติอย่างหนึ่งของ Python ข้อดีคือมันใช้การล็อกแบบทั่วโลกกับกระบวนการแปลภาษาแต่ละตัว ซึ่งหมายความว่าทุกกระบวนการจะปฏิบัติต่อ... Python ตัวแปลภาษาเองก็เป็นแหล่งข้อมูลอย่างหนึ่ง
ตัวอย่างเช่น สมมติว่าคุณได้เขียน Python โปรแกรมนี้ใช้สองเธรดในการดำเนินการทั้ง CPU และ 'I/O' เมื่อคุณรันโปรแกรมนี้ จะเกิดเหตุการณ์ดังนี้:
- การขอ Python ตัวแปลภาษาสร้างกระบวนการใหม่และสร้างเธรดขึ้นมา
- เมื่อเธรด-1 เริ่มทำงาน มันจะรับ GIL และล็อคมันก่อน
- หากเธรด-2 ต้องการดำเนินการตอนนี้ จะต้องรอให้ GIL เปิดตัว แม้ว่าโปรเซสเซอร์อื่นจะว่างก็ตาม
- สมมติว่าเธรด 1 กำลังรอการดำเนินการ I/O ในขณะนี้ เธรด 2 จะปล่อย GIL และเธรด XNUMX จะเข้าควบคุม GIL
- หลังจากดำเนินการ I/O เสร็จสิ้นแล้ว หาก thread-1 ต้องการดำเนินการตอนนี้ จะต้องรอให้ GIL ถูกรีลีสโดย thread-2 อีกครั้ง
ด้วยเหตุนี้ จึงมีเพียงเธรดเดียวเท่านั้นที่สามารถเข้าถึงตัวแปลภาษาได้ในแต่ละครั้ง ซึ่งหมายความว่าจะมีเพียงเธรดเดียวที่กำลังทำงานอยู่ Python รหัส ณ จุดเวลาใดเวลาหนึ่ง
วิธีการนี้ใช้ได้ดีกับโปรเซสเซอร์แบบแกนเดี่ยว เพราะจะใช้การแบ่งเวลา (ดูส่วนแรกของบทช่วยสอนนี้) ในการจัดการเธรด อย่างไรก็ตาม ในกรณีของโปรเซสเซอร์แบบหลายแกน ฟังก์ชันที่ใช้ CPU จำนวนมากซึ่งทำงานบนหลายเธรดจะส่งผลกระทบอย่างมากต่อประสิทธิภาพของโปรแกรม เนื่องจากจะไม่ใช้แกนประมวลผลทั้งหมดที่มีอยู่พร้อมกัน
เหตุใด GIL จึงจำเป็น?
คPython ตัวเก็บขยะ (Garbage Collector) ใช้เทคนิคการจัดการหน่วยความจำที่มีประสิทธิภาพที่เรียกว่าการนับการอ้างอิง (Reference Counting) วิธีการทำงานมีดังนี้: ทุกอ็อบเจ็กต์ใน Python อ็อบเจ็กต์จะมีจำนวนการอ้างอิง ซึ่งจะเพิ่มขึ้นเมื่อมีการกำหนดค่าให้กับตัวแปรใหม่หรือเพิ่มเข้าไปในคอนเทนเนอร์ (เช่น ทูเปิล ลิสต์ เป็นต้น) ในทำนองเดียวกัน จำนวนการอ้างอิงจะลดลงเมื่อการอ้างอิงอยู่นอกขอบเขตหรือเมื่อมีการเรียกใช้คำสั่ง del เมื่อจำนวนการอ้างอิงของอ็อบเจ็กต์ถึง 0 ระบบจะทำการเก็บกวาดขยะและปล่อยหน่วยความจำที่จัดสรรไว้ให้
แต่ปัญหาคือตัวแปรนับการอ้างอิงนั้นมีแนวโน้มที่จะเกิดสภาวะการแข่งขัน (race condition) เช่นเดียวกับตัวแปรส่วนกลางอื่นๆ เพื่อแก้ปัญหานี้ นักพัฒนาของ Python ตัดสินใจใช้การล็อกตัวแปลภาษาส่วนกลาง (global interpreter lock) ทางเลือกอื่นคือการเพิ่มการล็อกให้กับแต่ละอ็อบเจ็กต์ ซึ่งจะส่งผลให้เกิดภาวะการติดตาย (deadlock) และเพิ่มภาระงานจากการเรียกใช้ acquire() และ release()
ดังนั้น GIL จึงเป็นข้อจำกัดที่สำคัญสำหรับการประมวลผลแบบมัลติเธรด Python โปรแกรมที่ใช้การประมวลผล CPU อย่างหนัก (ทำให้โปรแกรมทำงานแบบเธรดเดียว) หากคุณต้องการใช้ CPU หลายคอร์ในแอปพลิเคชันของคุณ ให้ใช้... มัลติโปรเซสเซอร์ โมดูลแทน








