มัลติเธรดใน Python พร้อมตัวอย่าง: เรียนรู้ GIL ใน Python

⚡ สรุปอย่างชาญฉลาด

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

  • 🔘 เธรด กับ กระบวนการ: เธรดคือหน่วยการทำงานภายในกระบวนการ และเธรดแต่ละตัวจะแบ่งปันข้อมูลของกระบวนการในขณะที่ทำงานแยกกัน
  • ☑️ สองโมดูล: โมดูล _thread ที่ล้าสมัยนั้นมีไว้สำหรับการเรียกใช้งานระดับต่ำ ในขณะที่โมดูล threading ระดับสูงนั้นเป็นมาตรฐานสมัยใหม่สำหรับการสร้างเธรด
  • สร้างกระทู้: ขยายคลาส Thread เขียนทับเมธอด __init__ และ run() จากนั้นเรียก start() เพื่อเริ่มการทำงาน และเรียก join() เพื่อรอจนกว่าจะเสร็จสิ้น
  • 🧪 หลีกเลี่ยงสภาวะการแข่งขันของเวลา: กลไกการล็อกจะอนุญาตให้เธรดเพียงเธรดเดียวเข้าสู่ส่วนวิกฤตได้ในแต่ละครั้ง ซึ่งจะช่วยป้องกันภาวะการติดตายและการเสียหายของข้อมูลที่ใช้ร่วมกัน
  • 🛠️ ทำความเข้าใจ GIL: Global Interpreter Lock อนุญาตให้เธรดหนึ่งทำงานได้ Python ประมวลผลทีละไบต์โค้ด ดังนั้นงานที่ใช้ CPU มากจึงจำเป็นต้องใช้มัลติโปรเซสซิ่ง
  • 🤖 ภาระงาน AI: การทำงานแบบมัลติเธรดช่วยเพิ่มความเร็วให้กับขั้นตอน AI ที่เกี่ยวข้องกับการรับและส่งข้อมูล เช่น การโหลดข้อมูล ในขณะที่ NumPy และ PyTorch ปล่อย GIL สำหรับการคำนวณแบบขนาน

มัลติเธรดใน Python

การขอ Python ภาษาโปรแกรมอนุญาตให้คุณใช้การประมวลผลแบบหลายโปรเซสหรือมัลติเธรดดิ้ง ในบทเรียนนี้ คุณจะได้เรียนรู้วิธีการเขียนแอปพลิเคชันแบบมัลติเธรดดิ้งในภาษาโปรแกรม Python.

กระทู้คืออะไร?

เธรด (Thread) คือหน่วยการทำงานในการเขียนโปรแกรมแบบขนาน (Concurrent Programming) มัลติเธรดดิ้ง (Multithreading) เป็นเทคนิคที่ช่วยให้ซีพียูสามารถประมวลผลงานหลายอย่างของกระบวนการเดียวได้พร้อมกัน เธรดเหล่านี้สามารถทำงานแยกกันได้ในขณะที่แบ่งปันทรัพยากรของกระบวนการ

กระบวนการคืออะไร?

กระบวนการทำงานโดยพื้นฐานแล้วคือโปรแกรมที่กำลังทำงานอยู่ เมื่อคุณเริ่มใช้งานแอปพลิเคชันบนคอมพิวเตอร์ของคุณ (เช่น เว็บเบราว์เซอร์หรือโปรแกรมแก้ไขข้อความ) ระบบปฏิบัติการจะสร้างกระบวนการทำงานขึ้นมา กระบวนการ

มัลติเธรดคืออะไร Python?

มัลติเธรดใน Python การเขียนโปรแกรมแบบมัลติเธรดเป็นเทคนิคที่รู้จักกันดี ซึ่งเธรดหลายตัวในกระบวนการเดียวกันจะแบ่งปันพื้นที่ข้อมูลกับเธรดหลัก ทำให้การแบ่งปันข้อมูลและการสื่อสารระหว่างเธรดง่ายและมีประสิทธิภาพ เธรดมีน้ำหนักเบากว่ากระบวนการ เธรดหลายตัวสามารถทำงานแยกกันได้ในขณะที่แบ่งปันทรัพยากรของกระบวนการ จุดประสงค์ของการเขียนโปรแกรมแบบมัลติเธรดคือการเรียกใช้งานหลายงานและหลายฟังก์ชันพร้อมกัน

มัลติโปรเซสเซอร์คืออะไร?

การประมวลผลหลายขั้นตอน ช่วยให้คุณสามารถรันกระบวนการที่ไม่เกี่ยวข้องหลายกระบวนการพร้อมกันได้ กระบวนการเหล่านี้ไม่แบ่งปันทรัพยากรและสื่อสารกันผ่าน IPC

Python มัลติเธรดกับมัลติโปรเซสเซอร์

เพื่อให้เข้าใจกระบวนการและเธรด ลองพิจารณาสถานการณ์นี้: ไฟล์ .exe ในคอมพิวเตอร์ของคุณคือโปรแกรม เมื่อคุณเปิดมัน ระบบปฏิบัติการจะโหลดมันเข้าสู่หน่วยความจำ และซีพียูจะประมวลผลมัน อินสแตนซ์ของโปรแกรมที่กำลังทำงานอยู่นั้นเรียกว่ากระบวนการ

ทุกกระบวนการมีองค์ประกอบพื้นฐานสองอย่าง:

  • การขอ Code
  • ข้อมูล

ตอนนี้กระบวนการสามารถมีส่วนย่อยหนึ่งหรือหลายส่วนที่เรียกว่า หัวข้อ ขึ้นอยู่กับสถาปัตยกรรมของระบบปฏิบัติการ คุณอาจมองว่าเธรดเป็นส่วนหนึ่งของกระบวนการที่ระบบปฏิบัติการสามารถเรียกใช้งานแยกต่างหากได้

กล่าวอีกนัยหนึ่งคือ มันคือชุดคำสั่งที่ระบบปฏิบัติการสามารถประมวลผลได้อย่างอิสระ เธรดภายในกระบวนการเดียวจะใช้ข้อมูลร่วมกันของกระบวนการนั้น และถูกออกแบบมาให้ทำงานร่วมกันเพื่ออำนวยความสะดวกในการประมวลผลแบบขนาน

เหตุใดจึงต้องใช้มัลติเธรด?

มัลติเธรดช่วยให้คุณแบ่งแอปพลิเคชันออกเป็นหลายงานย่อยและรันงานเหล่านี้พร้อมกันได้ หากคุณใช้มัลติเธรดอย่างเหมาะสม ความเร็ว ประสิทธิภาพ และการเรนเดอร์ของแอปพลิเคชันของคุณก็จะดีขึ้น

Python มัลติเธรด

Python รองรับโครงสร้างทั้งสำหรับการประมวลผลแบบหลายโปรเซสและการทำงานแบบมัลติเธรด ในบทเรียนนี้ คุณจะเน้นไปที่การใช้งานเป็นหลัก มัลติเธรด แอพพลิเคชั่นด้วย Pythonมีโมดูลหลักสองโมดูลที่สามารถใช้จัดการเธรดได้ Python:

  1. การขอ ด้าย โมดูลและ
  2. การขอ เกลียว โมดูล

อย่างไรก็ตามใน 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 เพื่อรันโปรแกรม หากทุกอย่างถูกต้อง นี่คือผลลัพธ์ที่คุณควรเห็น:

โมดูลเธรด

คุณจะได้เรียนรู้เพิ่มเติมเกี่ยวกับสภาวะการแข่งขันและวิธีการรับมือกับสภาวะเหล่านั้นในส่วนถัดไป

โมดูลเธรด

คำอธิบายรหัส

  1. คำสั่งเหล่านี้นำเข้าโมดูลเวลาและเธรด ซึ่งใช้ในการจัดการการทำงานและการหน่วงเวลาของโปรแกรม Python หัวข้อ
  2. ที่นี่คุณได้กำหนดฟังก์ชันที่เรียกว่า เธรด_ทดสอบ, ซึ่งจะถูกเรียกโดย start_new_thread เมธอดนี้จะทำงานในลูป while เป็นเวลาสี่รอบ และพิมพ์ชื่อของเธรดที่เรียกใช้ เมื่อการทำงานเสร็จสิ้น จะแสดงข้อความว่าเธรดนั้นทำงานเสร็จแล้ว
  3. นี่คือส่วนหลักของโปรแกรมของคุณ ที่นี่คุณเพียงแค่โทรหา start_new_thread วิธีการกับ thread_test ฟังก์ชันเป็นอาร์กิวเมนต์ การทำเช่นนี้จะสร้างเธรดใหม่สำหรับฟังก์ชันที่คุณส่งเป็นอาร์กิวเมนต์และเริ่มทำงาน โปรดทราบว่าคุณสามารถแทนที่ (thread_test) ด้วยฟังก์ชันอื่นใดที่คุณต้องการเรียกใช้ในเธรดได้

โมดูลการทำเกลียว

โมดูลนี้เป็นการนำเอาการทำงานแบบมัลติเธรดมาใช้ในระดับสูงในภาษาโปรแกรม Python และเป็นมาตรฐานที่ใช้กันอย่างแพร่หลายในการจัดการแอปพลิเคชันแบบมัลติเธรด โดยมีคุณสมบัติหลากหลายกว่าโมดูลเธรด

โครงสร้างของโมดูลการทำเกลียว

โครงสร้างของโมดูลการทำเกลียว

นี่คือรายการฟังก์ชันที่มีประโยชน์บางส่วนที่กำหนดไว้ในโมดูลนี้:

ชื่อฟังก์ชั่น Descriptไอออน
ใช้งานนับ() ส่งคืนค่าการนับของ ด้าย สิ่งของที่ยังมีชีวิตอยู่
ปัจจุบันกระทู้() ส่งกลับวัตถุปัจจุบันของคลาสเธรด
การระบุ () แสดงรายการวัตถุ Thread ที่ใช้งานอยู่ทั้งหมด
isDaemon() คืนค่าเป็นจริงหากเธรดเป็น daemon
ยังมีชีวิตอยู่() คืนค่าเป็นจริงหากเธรดยังมีชีวิตอยู่
วิธีการคลาสเธรด
เริ่ม () เริ่มต้นกิจกรรมของเธรด จะต้องถูกเรียกเพียงครั้งเดียวสำหรับแต่ละเธรด เนื่องจากจะทำให้เกิดข้อผิดพลาดรันไทม์หากถูกเรียกหลายครั้ง
วิ่ง() เมธอดนี้แสดงถึงกิจกรรมของเธรดและสามารถแทนที่ได้โดยคลาสที่ขยายคลาสเธรด
เข้าร่วม () มันจะบล็อกการดำเนินการของโค้ดอื่น ๆ จนกระทั่งเธรดที่เมธอด join() ถูกเรียกถูกยกเลิก

เรื่องราวเบื้องหลัง: คลาสเธรด

ก่อนที่คุณจะเริ่มเขียนโปรแกรมแบบมัลติเธรดโดยใช้โมดูล threading คุณจำเป็นต้องเข้าใจคลาส Thread ก่อน คลาส Thread เป็นคลาสหลักที่กำหนดเทมเพลตและการทำงานของเธรด Python.

วิธีที่พบได้บ่อยที่สุดในการสร้างมัลติเธรด Python แอปพลิเคชันนี้คือการประกาศคลาสที่สืบทอดมาจากคลาส Thread และเขียนทับเมธอด run() ของคลาส Thread นั้น

โดยสรุปคลาส Thread หมายถึงลำดับโค้ดที่ทำงานแยกกัน ด้าย ของการควบคุม

ดังนั้นเมื่อเขียนแอปแบบมัลติเธรด คุณจะต้องทำสิ่งต่อไปนี้:

  1. กำหนดคลาสที่สืบทอดมาจากคลาส Thread
  2. แทนที่ __ในนั้น__ นวกรรมิก
  3. แทนที่ วิ่ง() วิธี

เมื่อสร้างวัตถุเธรดแล้ว เริ่ม () สามารถใช้วิธีนี้ในการเริ่มต้นการดำเนินการกิจกรรมนี้ และ เข้าร่วม () สามารถใช้เมธอดนี้เพื่อบล็อกโค้ดอื่นๆ ทั้งหมดได้จนกว่ากิจกรรมปัจจุบันจะเสร็จสิ้น

ทีนี้ เรามาลองใช้โมดูล 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()

นี่จะเป็นผลลัพธ์เมื่อคุณรันโค้ดด้านบน:

เรื่องราวเบื้องหลัง: คลาสเธรด

คำอธิบายรหัส

เรื่องราวเบื้องหลัง: คลาสเธรด

  1. ส่วนนี้เหมือนกับตัวอย่างก่อนหน้านี้ ตรงนี้ คุณจะนำเข้าโมดูลเวลาและเธรด ซึ่งใช้ในการจัดการการทำงานและการหน่วงเวลา Python หัวข้อ
  2. ในบิตนี้ คุณกำลังสร้างคลาสที่เรียกว่า threadtester ซึ่งจะสืบทอดหรือขยาย ด้าย คลาสของโมดูล threading นี่เป็นหนึ่งในวิธีที่พบบ่อยที่สุดในการสร้างเธรดใน Pythonอย่างไรก็ตาม คุณควรเขียนทับเฉพาะคอนสตรัคเตอร์และเท่านั้น วิ่ง() วิธีการในแอปของคุณ ดังที่คุณเห็นในตัวอย่างโค้ดข้างต้น __ในนั้น__ วิธีการ (ตัวสร้าง) ได้รับการแทนที่แล้ว ในทำนองเดียวกัน คุณได้แทนที่ด้วย วิ่ง() วิธี. ประกอบด้วยโค้ดที่คุณต้องการดำเนินการภายในเธรด ในตัวอย่างนี้ คุณได้เรียกใช้ฟังก์ชัน thread_test()
  3. นี่คือเมธอด thread_test() ซึ่งรับค่าของ i โดยรับค่า i เป็นอาร์กิวเมนต์ จะลดค่าลง 1 ในแต่ละรอบการวนซ้ำ และวนลูปผ่านส่วนที่เหลือของโค้ดจนกว่า i จะเป็น 0 ในแต่ละรอบการวนซ้ำ จะพิมพ์ชื่อของเธรดที่กำลังทำงานอยู่ และหยุดการทำงานเป็นเวลา wait วินาที (ซึ่งรับเป็นอาร์กิวเมนต์เช่นกัน)
  4. thread1 = threadtester(1, “First Thread”, 1) ที่นี่ เรากำลังสร้างเธรดและส่งผ่านพารามิเตอร์ทั้งสามที่เราประกาศใน __init__ พารามิเตอร์แรกคือ id ของเธรด พารามิเตอร์ที่สองคือชื่อของเธรด และพารามิเตอร์ที่สามคือตัวนับ ซึ่งจะกำหนดจำนวนครั้งที่ลูป while ควรรัน
  5. thread2.start() เมธอด start ใช้สำหรับเริ่มการทำงานของเธรด ภายในแล้ว ฟังก์ชัน start() จะเรียกเมธอด run() ของคลาสของคุณ
  6. thread3.join() วิธีการ join() บล็อกการทำงานของโค้ดอื่น ๆ และรอจนกว่าเธรดที่ถูกเรียกว่าเสร็จสิ้น

อย่างที่คุณทราบแล้ว เธรดที่อยู่ในกระบวนการเดียวกันจะสามารถเข้าถึงหน่วยความจำและข้อมูลของกระบวนการนั้นได้ ดังนั้น หากมีเธรดมากกว่าหนึ่งเธรดพยายามเปลี่ยนแปลงหรือเข้าถึงข้อมูลพร้อมกัน อาจทำให้เกิดข้อผิดพลาดได้

ในส่วนถัดไป คุณจะได้เห็นปัญหาต่างๆ ที่อาจเกิดขึ้นเมื่อเธรดเข้าถึงข้อมูลและส่วนวิกฤตโดยไม่ตรวจสอบธุรกรรมการเข้าถึงที่มีอยู่ก่อน

เดดล็อคและสภาวะการแข่งขัน

ก่อนที่จะเรียนรู้เกี่ยวกับภาวะติดตาย (deadlock) และภาวะการแข่งขัน (race condition) จะเป็นประโยชน์อย่างยิ่งหากเราเข้าใจคำจำกัดความพื้นฐานบางประการที่เกี่ยวข้องกับการเขียนโปรแกรมแบบขนาน:

  • ส่วนสำคัญ: นี่คือส่วนของโค้ดที่เข้าถึงหรือแก้ไขตัวแปรที่ใช้ร่วมกัน และต้องดำเนินการในรูปแบบธุรกรรมอะตอมิกเท่านั้น
  • การสลับบริบท: เป็นกระบวนการที่ซีพียูใช้ในการจัดเก็บสถานะของเธรดก่อนที่จะเปลี่ยนจากงานหนึ่งไปอีกงานหนึ่ง เพื่อให้สามารถกลับมาทำงานต่อจากจุดเดิมได้ในภายหลัง

การหยุดชะงัก

การหยุดชะงัก เป็นปัญหาที่นักพัฒนาซอฟต์แวร์กลัวมากที่สุดเมื่อเขียนแอปพลิเคชันแบบขนาน/มัลติเธรด Pythonวิธีที่ดีที่สุดในการทำความเข้าใจภาวะติดตาย (deadlock) คือการใช้ตัวอย่างปัญหาคลาสสิกในวิทยาการคอมพิวเตอร์ที่รู้จักกันในชื่อ... ห้องอาหาร Philoปัญหาโซเฟอร์

คำชี้แจงปัญหาสำหรับนักปรัชญาการรับประทานอาหารมีดังนี้:

ดังแสดงในแผนภาพ นักปรัชญาห้าคนนั่งอยู่ที่โต๊ะกลม โดยมีจานสปาเก็ตตี้ (พาสต้าชนิดหนึ่ง) ห้าจานและส้อมห้าอันวางอยู่บนโต๊ะ

ห้องอาหาร 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 ปลดล็อค รองรับสองวิธี:

  1. ได้รับ(): เมื่อสถานะการล็อกถูกปลดล็อก การเรียกเมธอด acquire() จะเปลี่ยนสถานะเป็นล็อกและส่งคืนค่า อย่างไรก็ตาม หากสถานะถูกล็อก การเรียก acquire() จะถูกบล็อกจนกว่าเมธอด release() จะถูกเรียกโดยเธรดอื่น
  2. ปล่อย(): 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 คุณควรเห็นผลลัพธ์ดังนี้:

Syncหัวข้อการขัดจังหวะ

คำอธิบายรหัส

Syncหัวข้อการขัดจังหวะ

  1. ที่นี่ คุณเพียงแค่สร้างล็อคใหม่โดยการโทรไปที่ เกลียวล็อค() ฟังก์ชั่นโรงงาน ภายใน Lock() ส่งคืนอินสแตนซ์ของคลาส Lock ที่เป็นรูปธรรมที่มีประสิทธิภาพสูงสุดซึ่งดูแลโดยแพลตฟอร์ม
  2. ในคำสั่งแรก คุณจะได้รับล็อคโดยการเรียกเมธอด Acquis() เมื่ออนุญาตให้ล็อคแล้ว คุณจะพิมพ์ “ได้รับล็อคแล้ว” ไปที่คอนโซล เมื่อโค้ดทั้งหมดที่คุณต้องการให้เธรดทำงานเสร็จสิ้นแล้ว คุณสามารถปลดล็อคโดยการเรียกเมธอด release()

ทฤษฎีนั้นถูกต้อง แต่คุณจะรู้ได้อย่างไรว่าการล็อกนั้นได้ผลจริง ๆ? ถ้าคุณดูที่ผลลัพธ์ คุณจะเห็นว่าคำสั่ง print แต่ละคำสั่งจะพิมพ์ออกมาทีละบรรทัดเท่านั้น จำได้ไหมว่าในตัวอย่างก่อนหน้านี้ ผลลัพธ์จาก print นั้นกระจัดกระจายเพราะมีหลายเธรดเข้าถึงเมธอด print() พร้อมกัน แต่ในที่นี้ ฟังก์ชัน print จะถูกเรียกใช้หลังจากที่ได้ล็อกแล้วเท่านั้น ดังนั้นผลลัพธ์จึงแสดงออกมาทีละบรรทัด

นอกเหนือจากกุญแจแล้ว Python นอกจากนี้ยังรองรับกลไกอื่นๆ ในการจัดการการซิงโครไนซ์เธรด ดังที่ระบุไว้ด้านล่าง:

  1. อาร์ล็อคส์
  2. Semaphores
  3. เงื่อนไขเพิ่มเติมสำหรับการขอคืนเงิน (Refund Policy)
  4. เหตุการณ์และ
  5. ปัญหาและอุปสรรคที่

Global Interpreter Lock (และวิธีจัดการกับมัน)

ก่อนที่จะลงรายละเอียด Pythonเพื่อนๆ ใน GIL ​​เรามานิยามคำศัพท์บางคำที่จะเป็นประโยชน์ในการทำความเข้าใจหัวข้อต่อไปกัน:

  1. โค้ดที่ประมวลผลโดย CPU: หมายถึงโค้ดใดๆ ก็ตามที่จะถูกประมวลผลโดยตรงโดย CPU
  2. โค้ดที่เน้นการรับส่งข้อมูล (I/O-bound code): นี่คือโค้ดใดๆ ก็ตามที่เข้าถึงระบบไฟล์ผ่านทางระบบปฏิบัติการ
  3. CPython: มันเป็นข้อมูลอ้างอิง การดำเนินงาน of Python และสามารถอธิบายได้ว่าเป็นล่ามที่เขียนด้วยภาษาซีและ Python (ภาษาโปรแกรม).

GIL คืออะไร Python?

ล็อคล่ามสากล (GIL) in Python GIL (Global Interpreter Lock) คือตัวล็อกกระบวนการหรือมิวเท็กซ์ที่ใช้ในการจัดการกับกระบวนการต่างๆ มันช่วยให้มั่นใจได้ว่าเธรดหนึ่งๆ เท่านั้นที่จะสามารถเข้าถึงทรัพยากรใดทรัพยากรหนึ่งได้ในแต่ละครั้ง และยังป้องกันการใช้งานอ็อบเจ็กต์และไบต์โค้ดพร้อมกัน ซึ่งเป็นประโยชน์ต่อโปรแกรมแบบเธรดเดียวด้วยประสิทธิภาพที่เพิ่มขึ้น GIL ​​ใน Python มันง่ายมากและนำไปใช้งานได้ง่าย

สามารถใช้การล็อกเพื่อให้แน่ใจว่ามีเธรดเดียวเท่านั้นที่สามารถเข้าถึงทรัพยากรเฉพาะในเวลาที่กำหนด

คุณสมบัติอย่างหนึ่งของ Python ข้อดีคือมันใช้การล็อกแบบทั่วโลกกับกระบวนการแปลภาษาแต่ละตัว ซึ่งหมายความว่าทุกกระบวนการจะปฏิบัติต่อ... Python ตัวแปลภาษาเองก็เป็นแหล่งข้อมูลอย่างหนึ่ง

ตัวอย่างเช่น สมมติว่าคุณได้เขียน Python โปรแกรมนี้ใช้สองเธรดในการดำเนินการทั้ง CPU และ 'I/O' เมื่อคุณรันโปรแกรมนี้ จะเกิดเหตุการณ์ดังนี้:

  1. การขอ Python ตัวแปลภาษาสร้างกระบวนการใหม่และสร้างเธรดขึ้นมา
  2. เมื่อเธรด-1 เริ่มทำงาน มันจะรับ GIL และล็อคมันก่อน
  3. หากเธรด-2 ต้องการดำเนินการตอนนี้ จะต้องรอให้ GIL เปิดตัว แม้ว่าโปรเซสเซอร์อื่นจะว่างก็ตาม
  4. สมมติว่าเธรด 1 กำลังรอการดำเนินการ I/O ในขณะนี้ เธรด 2 จะปล่อย GIL และเธรด XNUMX จะเข้าควบคุม GIL
  5. หลังจากดำเนินการ 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 หลายคอร์ในแอปพลิเคชันของคุณ ให้ใช้... มัลติโปรเซสเซอร์ โมดูลแทน

คำถามที่พบบ่อย

ใช้เธรดดิ้งสำหรับงานที่เน้นการรับส่งข้อมูล เช่น การเรียกใช้เครือข่าย การเข้าถึงไฟล์ หรือการสืบค้นฐานข้อมูล ซึ่งเธรดจะรอทรัพยากรภายนอก ใช้มัลติโปรเซสซิ่งสำหรับงานที่เน้นการประมวลผลของ CPU เช่น การคำนวณที่ซับซ้อน เพราะมันทำงานบนหลายคอร์และข้าม GIL (Global Interpreter List)

ไม่ใช่โดยค่าเริ่มต้น PEP 703 ได้แนะนำการสร้างแบบใช้เธรดอิสระที่เป็นตัวเลือกเพิ่มเติม Python 3.13 ที่ปิดใช้งาน GIL ​​และ Python เวอร์ชัน 3.14 ยังคงใช้ GIL อยู่ การสร้างเวอร์ชันมาตรฐานยังคงใช้ GIL ดังนั้นโปรแกรมส่วนใหญ่จึงยังคงทำงานได้เหมือนเดิมทุกประการ

เธรดแบบดีมอนจะทำงานอยู่เบื้องหลังและไม่ขัดขวางการปิดโปรแกรม เมื่อเหลือเพียงเธรดแบบดีมอนเท่านั้น Python ปิดการทำงานและออกจากโปรแกรม ตั้งค่า thread.daemon = True ก่อนเรียก start()

คลาส Thread ไม่ส่งคืนผลลัพธ์ของ run() โดยตรง ให้เก็บค่าไว้ในอ็อบเจ็กต์ thread หรือคิวที่ใช้ร่วมกัน หรือใช้ concurrent.futures.ThreadPoolExecutor ซึ่งเมธอด submit() จะส่งคืน Future ที่คุณสามารถอ่านได้ด้วย result()

ThreadPoolExecutor เป็นตัวช่วยระดับสูงที่จัดการกลุ่มเธรดทำงานให้คุณ คุณส่งงานด้วย submit() หรือ map() และมันจะจัดการการสร้าง การนำกลับมาใช้ใหม่ และการล้างเธรด โดยส่งคืนอ็อบเจ็กต์ Future ที่เก็บผลลัพธ์แต่ละรายการ

คุณสามารถสร้างเธรดได้หลายร้อยหรือหลายพันเธรด แต่ GIL อนุญาตให้ทำงานได้เพียงเธรดเดียวเท่านั้น Python ไบต์โค้ด ณ ขณะใดขณะหนึ่ง พวกมันยังคงทับซ้อนกันในระหว่างการรอ I/O ดังนั้นเธรดพร้อมกันจำนวนมากจึงช่วยโปรแกรมที่เน้น I/O ได้มากกว่าโปรแกรมที่เน้น CPU มาก

ใช่ครับ ส่วนใหญ่ใช้สำหรับขั้นตอนที่เน้นการรับส่งข้อมูล เช่น การโหลดข้อมูล การเรียกใช้ API หรือการอ่านไฟล์ ส่วนการฝึกข้อมูลจำนวนมากจะเน้นการทำงานของ CPU ดังนั้นจึงใช้ไลบรารีอย่าง NumPy และ Py ครับTorch ปลดปล่อย GIL ในโค้ด C หรืออาศัยมัลติโปรเซสซิ่งเพื่อให้ได้การทำงานแบบขนานอย่างแท้จริง

ใช่แล้ว GitHub Copilot จะเติมโค้ดอัตโนมัติตามรูปแบบทั่วไป เช่น คลาสย่อยของ Thread การใช้งาน Lock และการตั้งค่า ThreadPoolExecutor จากความคิดเห็นหรือชื่อฟังก์ชัน ควรตรวจสอบโค้ดที่สร้างขึ้นเพื่อหาเงื่อนไขการแข่งขัน (race condition) และการจัดการ Lock ที่ถูกต้องก่อนที่จะนำไปใช้งาน

สรุปโพสต์นี้ด้วย: