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

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

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

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

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

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

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

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

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

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

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

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

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

  • รหัส
  • ข้อมูล

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

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

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

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

Python มัลติเธรด

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

  1. เทศกาล ด้าย โมดูลและ
  2. เทศกาล เกลียว โมดูล

อย่างไรก็ตาม ใน python ยังมีสิ่งที่เรียกว่า global reaperter 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 loop สี่รอบและพิมพ์ชื่อของเธรดที่เรียกว่าเธรดนั้น เมื่อการวนซ้ำเสร็จสิ้น ระบบจะพิมพ์ข้อความแจ้งว่าเธรดเสร็จสิ้นการดำเนินการแล้ว
  3. นี่คือส่วนหลักของโปรแกรมของคุณ ที่นี่คุณเพียงแค่โทรหา start_new_thread วิธีการกับ thread_test ทำหน้าที่เป็นอาร์กิวเมนต์ ซึ่งจะสร้างเธรดใหม่สำหรับฟังก์ชันที่คุณส่งเป็นอาร์กิวเมนต์ และเริ่มดำเนินการ โปรดทราบว่าคุณสามารถแทนที่สิ่งนี้ได้ (thread_test) กับฟังก์ชันอื่นๆ ที่คุณต้องการเรียกใช้เป็นเธรด

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

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

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

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

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

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

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

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

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

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

  1. กำหนดคลาสที่ขยายคลาสเธรด
  2. แทนที่ __ในนั้น__ นวกรรมิก
  3. แทนที่ วิ่ง() วิธี

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

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

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

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

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

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

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

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

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

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

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

ห้องอาหาร Philoปัญหาโซเฟอร์

ห้องอาหาร Philoปัญหาโซเฟอร์

ในช่วงเวลาใดก็ตาม นักปรัชญาจะต้องกินหรือกำลังคิด

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

เนื่องจากนักปรัชญาแต่ละคนมีส้อมคนละหนึ่งอัน พวกเขาจึงต้องรอให้คนอื่นวางส้อมลง ผลก็คือไม่มีใครกินสปาเก็ตตี้ได้

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

เงื่อนไขการแข่งขัน

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

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

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

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

Syncหัวข้อการขัดสี

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

ล็อคคือการซิงโครไนซ์ระดับต่ำแบบดั้งเดิมที่ใช้งานโดย __เกลียว โมดูล. ในเวลาใดก็ตาม การล็อกอาจอยู่ในสถานะใดสถานะหนึ่งจาก 2 สถานะ: ล็อค or ปลดล็อค รองรับสองวิธี:

  1. ได้รับ()เมื่อสถานะล็อคถูกปลดล็อค การเรียกวิธีการรับ() จะเปลี่ยนสถานะเป็นล็อคและส่งคืน อย่างไรก็ตาม หากสถานะถูกล็อค การเรียกเพื่อรับ() จะถูกบล็อกจนกว่าเมธอด 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() ในเวลาเดียวกัน ในที่นี้ ฟังก์ชันพิมพ์จะถูกเรียกใช้หลังจากล็อกแล้วเท่านั้น ดังนั้น ผลลัพธ์จะแสดงทีละรายการและทีละบรรทัด

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

  1. อาร์ล็อคส์
  2. Semaphores
  3. เงื่อนไข
  4. เหตุการณ์และ
  5. ปัญหาและอุปสรรคที่

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

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

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

GIL คืออะไร Python?

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

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

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

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

  1. ล่ามหลามสร้างกระบวนการใหม่และวางไข่เธรด
  2. เมื่อเธรด-1 เริ่มทำงาน มันจะรับ GIL และล็อคมันก่อน
  3. หากเธรด-2 ต้องการดำเนินการตอนนี้ จะต้องรอให้ GIL เปิดตัว แม้ว่าโปรเซสเซอร์อื่นจะว่างก็ตาม
  4. สมมติว่าเธรด 1 กำลังรอการดำเนินการ I/O ในขณะนี้ เธรด 2 จะปล่อย GIL และเธรด XNUMX จะเข้าควบคุม GIL
  5. หลังจากดำเนินการ I/O เสร็จสิ้นแล้ว หาก thread-1 ต้องการดำเนินการตอนนี้ จะต้องรอให้ GIL ถูกรีลีสโดย thread-2 อีกครั้ง

ด้วยเหตุนี้ มีเพียงเธรดเดียวเท่านั้นที่สามารถเข้าถึงล่ามได้ตลอดเวลา ซึ่งหมายความว่าจะมีเธรดเดียวเท่านั้นที่รันโค้ดหลาม ณ เวลาที่กำหนด

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

เหตุใด GIL จึงจำเป็น?

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

แต่ปัญหาคือตัวแปรจำนวนการอ้างอิงนั้นมีแนวโน้มที่จะเกิดสภาวะการแข่งขันเช่นเดียวกับตัวแปรทั่วโลกอื่นๆ เพื่อแก้ปัญหานี้ นักพัฒนา Python จึงตัดสินใจใช้การล็อกอินเทอร์พรีเตอร์ทั่วโลก อีกทางเลือกหนึ่งคือการเพิ่มการล็อกให้กับแต่ละอ็อบเจ็กต์ซึ่งจะส่งผลให้เกิดเดดล็อกและเพิ่มค่าใช้จ่ายจากการเรียกใช้ acquire() และ release()

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

สรุป

  • Python รองรับ 2 โมดูลสำหรับมัลติเธรด:
    1. __เกลียว โมดูล: มีการใช้งานระดับต่ำสำหรับเธรดและล้าสมัย
    2. โมดูลเกลียว: ให้การใช้งานระดับสูงสำหรับมัลติเธรดและเป็นมาตรฐานปัจจุบัน
  • ในการสร้างเธรดโดยใช้โมดูลเธรด คุณต้องดำเนินการดังต่อไปนี้:
    1. สร้างคลาสที่ขยาย ด้าย ชั้นเรียน
    2. แทนที่ตัวสร้างของมัน (__init__)
    3. แทนที่มัน วิ่ง() วิธี
    4. สร้างวัตถุของคลาสนี้
  • เธรดสามารถดำเนินการได้โดยการเรียก เริ่ม () วิธี
  • เทศกาล เข้าร่วม () สามารถใช้เมธอดนี้เพื่อบล็อกเธรดอื่นได้จนกว่าเธรดนี้ (เธรดที่ถูกเรียกใช้การรวม) จะดำเนินการเสร็จสิ้น
  • สภาวะการแข่งขันเกิดขึ้นเมื่อหลายเธรดเข้าถึงหรือแก้ไขทรัพยากรที่ใช้ร่วมกันในเวลาเดียวกัน
  • สามารถหลีกเลี่ยงได้โดย Syncหัวข้อการขัดสี
  • Python รองรับ 6 วิธีในการซิงโครไนซ์เธรด:
    1. ล็อค
    2. อาร์ล็อคส์
    3. Semaphores
    4. เงื่อนไข
    5. เหตุการณ์และ
    6. ปัญหาและอุปสรรคที่
  • ล็อคอนุญาตให้เฉพาะเธรดเฉพาะที่ได้รับล็อคเพื่อเข้าสู่ส่วนที่สำคัญ
  • การล็อคมี 2 วิธีหลัก:
    1. ได้รับ(): ตั้งค่าสถานะล็อคเป็น ล็อค หากเรียกใช้บนวัตถุที่ถูกล็อค มันจะบล็อกจนกว่าทรัพยากรจะว่าง
    2. ปล่อย(): ตั้งค่าสถานะล็อคเป็น ปลดล็อค และกลับมา หากเรียกใช้บนวัตถุที่ปลดล็อคจะส่งคืนค่าเท็จ
  • การล็อคล่ามส่วนกลางเป็นกลไกที่มีเพียง 1 C เท่านั้นPython กระบวนการล่ามสามารถดำเนินการได้ในแต่ละครั้ง
  • มันถูกใช้เพื่ออำนวยความสะดวกให้กับฟังก์ชันการนับการอ้างอิงของ CPythonคนเก็บขยะของเอส
  • ที่จะทำให้ Python แอพที่มีการใช้งาน CPU หนักๆ ควรใช้โมดูลการประมวลผลหลายตัว