Python Threading: บทนำ

เผยแพร่แล้ว: 2022-10-25

ในบทช่วยสอนนี้ คุณจะได้เรียนรู้วิธีใช้โมดูล เธรด ในตัวของ Python เพื่อสำรวจความสามารถมัลติเธรดใน Python

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

มาเริ่มกันเลย.

กระบวนการกับเธรด: อะไรคือความแตกต่าง?

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

กระบวนการ คืออินสแตนซ์ ใดๆ ของโปรแกรมที่ต้องเรียกใช้

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

cpu-proc-เธรด

การทำความเข้าใจกระบวนการและเธรด

ภายใน กระบวนการมีหน่วยความจำเฉพาะที่เก็บรหัสและข้อมูลที่สอดคล้องกับกระบวนการ

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

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

กระบวนการและเธรด

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

จากสิ่งที่เราได้เรียนรู้ไปแล้ว มาสรุปความแตกต่างระหว่างกระบวนการและเธรด

ลักษณะเฉพาะ กระบวนการ เกลียว
หน่วยความจำ หน่วยความจำเฉพาะ หน่วยความจำที่ใช้ร่วมกัน
โหมดการดำเนินการ ขนานกัน พร้อมกัน; แต่ไม่ขนานกัน
ดำเนินการโดย ระบบปฏิบัติการ ล่าม CPython

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

ใน Python Global Interpreter Lock (GIL) ช่วยให้มั่นใจได้ว่า มีเพียง เธรดเดียวเท่านั้นที่สามารถรับการล็อกและเรียกใช้ได้ตลอดเวลา เธรดทั้งหมดควรได้รับล็อกนี้เพื่อเรียกใช้ สิ่งนี้ทำให้มั่นใจได้ว่ามีเพียงเธรดเดียวเท่านั้นที่สามารถดำเนินการได้— ณ เวลาใดก็ตาม—และหลีกเลี่ยงการทำมัลติเธรด พร้อมกัน

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

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

Concurrency vs. Parallelism: ภาพรวม

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

หากมีสี่โปรเซส แต่ละโปรเซสสามารถรันอย่างอิสระและพร้อมกันบนคอร์ทั้งสี่ตัวได้ สมมติว่าแต่ละกระบวนการมีสองเธรด

multicore-parallelism

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

รหัส

ตัวอย่างเช่น เธรดที่ผูกกับ I/O มักจะรอการดำเนินการของ I/O: การอ่านอินพุตของผู้ใช้ การอ่านฐานข้อมูล และการดำเนินการกับไฟล์ ในช่วงเวลารอนี้ สามารถ ปลด ล็อคเพื่อให้เธรดอื่นทำงาน เวลารอสามารถเป็นการดำเนินการง่ายๆ เช่น นอนเป็นเวลา n วินาที

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

หากคุณต้องการใช้การขนานระดับกระบวนการในแอปพลิเคชันของคุณ ให้ลองใช้การประมวลผลหลายตัวแทน

Python Threading Module: ขั้นตอนแรก

Python มาพร้อมกับโมดูล threading ที่คุณสามารถนำเข้าไปยังสคริปต์ Python ได้

 import threading

ในการสร้างวัตถุเธรดใน Python คุณสามารถใช้ตัวสร้าง Thread : threading.Thread(...) นี่คือไวยากรณ์ทั่วไปที่เพียงพอสำหรับการใช้งานเธรดส่วนใหญ่:

 threading.Thread(target=...,args=...)

ที่นี่,

  • target คืออาร์กิวเมนต์ของคีย์เวิร์ดซึ่งแสดงถึง Python callable
  • args คือ tuple ของอาร์กิวเมนต์ที่เป้าหมายรับเข้ามา

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

วิธีกำหนดและเรียกใช้เธรดใน Python

มากำหนดเธรดที่เรียกใช้ฟังก์ชันเป้าหมายกัน

ฟังก์ชั่นเป้าหมายคือ some_func

 import threading import time def some_func(): print("Running some_func...") time.sleep(2) print("Finished running some_func.") thread1 = threading.Thread(target=some_func) thread1.start() print(threading.active_count())

มาแยกวิเคราะห์ว่าข้อมูลโค้ดด้านบนนี้ทำอะไรได้บ้าง:

  • มันนำเข้า threading และโมดูล time
  • ฟังก์ชัน some_func มีคำสั่ง print() อธิบายและรวมถึงการดำเนินการสลีปเป็นเวลาสองวินาที: time.sleep(n) ทำให้ฟังก์ชันเข้าสู่โหมดสลีปเป็นเวลา n วินาที
  • ต่อไป เรากำหนด thread_1 โดยมีเป้าหมายเป็น some_func threading.Thread(target=...) สร้างวัตถุเธรด
  • หมายเหตุ : ระบุชื่อของฟังก์ชันและไม่ใช่การเรียกใช้ฟังก์ชัน ใช้ some_func ไม่ใช่ some_func()
  • การสร้างวัตถุเธรด ไม่ เริ่มเธรด เรียกเมธอด start() บนวัตถุเธรด
  • เพื่อให้ได้จำนวนเธรดที่ใช้งานอยู่ เราใช้ active_count()

สคริปต์ Python ทำงานบนเธรดหลัก และเรากำลังสร้างเธรดอื่น ( thread1 ) เพื่อเรียกใช้ฟังก์ชัน some_func ดังนั้นจำนวนเธรดที่แอ็คทีฟจึงเป็นสอง ดังที่เห็นในผลลัพธ์:

 # Output Running some_func... 2 Finished running some_func.

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

thread1-ex

รอให้เธรดดำเนินการเสร็จสิ้น

หากคุณต้องการให้ thread1 ดำเนินการเสร็จสิ้น คุณสามารถเรียกใช้เมธอด join() ได้หลังจากเริ่มเธรด การทำเช่นนี้จะรอให้ thread1 ดำเนินการเสร็จสิ้นโดยไม่ต้องเปลี่ยนไปใช้เธรดหลัก

 import threading import time def some_func(): print("Running some_func...") time.sleep(2) print("Finished running some_func.") thread1 = threading.Thread(target=some_func) thread1.start() thread1.join() print(threading.active_count())

ตอนนี้ thread1 ดำเนินการเสร็จสิ้นก่อนที่เราจะพิมพ์จำนวนเธรดที่ใช้งานอยู่ ดังนั้นเฉพาะเธรดหลักที่ทำงานอยู่ ซึ่งหมายความว่าจำนวนเธรดที่ใช้งานอยู่คือหนึ่งรายการ

 # Output Running some_func... Finished running some_func. 1

วิธีเรียกใช้หลายเธรดใน Python

ต่อไป มาสร้างสองเธรดเพื่อเรียกใช้ฟังก์ชันที่แตกต่างกันสองอย่าง

ที่นี่ count_down เป็นฟังก์ชันที่ใช้ตัวเลขเป็นอาร์กิวเมนต์และนับถอยหลังจากตัวเลขนั้นเป็นศูนย์

 def count_down(n): for i in range(n,-1,-1): print(i)

เรากำหนด count_up ซึ่งเป็นฟังก์ชัน Python อื่นที่นับจากศูนย์ถึงจำนวนที่กำหนด

 def count_up(n): for i in range(n+1): print(i)

เมื่อใช้ฟังก์ชัน range() กับ range(start, stop, step) จุดสิ้นสุดจุด stop จะถูกยกเว้นโดยค่าเริ่มต้น

– ในการนับถอยหลังจากตัวเลขที่ระบุถึงศูนย์ คุณสามารถใช้ค่า step ลบที่ -1 และตั้งค่า stop เป็น -1 เพื่อให้ศูนย์รวมอยู่ด้วย

– ในทำนองเดียวกัน หากต้องการนับถึง n คุณต้องตั้งค่า stop เป็น n + 1 เนื่องจากค่าเริ่มต้นของการ start และ step คือ 0 และ 1 ตามลำดับ คุณอาจใช้ range(n + 1) เพื่อรับลำดับ 0 ถึง n

ต่อไป เรากำหนดสองเธรด thread1 และ thread2 เพื่อเรียกใช้ฟังก์ชัน count_down และ count_up ตามลำดับ เราเพิ่มคำสั่ง print และการดำเนินการส sleep ปสำหรับทั้งสองฟังก์ชัน

เมื่อสร้างออบเจ็กต์เธรด โปรดสังเกตว่าอาร์กิวเมนต์ของฟังก์ชันเป้าหมายควรระบุเป็นทูเพิล—สำหรับพารามิเตอร์ args เนื่องจากทั้งฟังก์ชัน ( count_down และ count_up ) รับอาร์กิวเมนต์เดียว คุณจะต้องใส่เครื่องหมายจุลภาคหลังค่าอย่างชัดเจน เพื่อให้แน่ใจว่าอาร์กิวเมนต์ยังคงส่งผ่านเป็นทูเพิล เนื่องจากองค์ประกอบที่ตามมาจะถูกอนุมานว่า None

 import threading import time def count_down(n): for i in range(n,-1,-1): print("Running thread1....") print(i) time.sleep(1) def count_up(n): for i in range(n+1): print("Running thread2...") print(i) time.sleep(1) thread1 = threading.Thread(target=count_down,args=(10,)) thread2 = threading.Thread(target=count_up,args=(5,)) thread1.start() thread2.start()

ในผลลัพธ์:

  • ฟังก์ชัน count_up ทำงานบน thread2 และนับได้ถึง 5 โดยเริ่มจาก 0
  • ฟังก์ชัน count_down ทำงานบน thread1 นับถอยหลังจาก 10 ถึง 0
 # Output Running thread1.... 10 Running thread2... 0 Running thread1.... 9 Running thread2... 1 Running thread1.... 8 Running thread2... 2 Running thread1.... 7 Running thread2... 3 Running thread1.... 6 Running thread2... 4 Running thread1.... 5 Running thread2... 5 Running thread1.... 4 Running thread1.... 3 Running thread1.... 2 Running thread1.... 1 Running thread1.... 0

คุณสามารถเห็นได้ว่า thread1 และ thread2 ทำงานอย่างอื่น เนื่องจากทั้งคู่เกี่ยวข้องกับการดำเนินการรอ (สลีป) เมื่อฟังก์ชัน count_up นับได้ถึง 5 เสร็จแล้ว thread2 จะไม่ทำงานอีกต่อไป ดังนั้นเราจึงได้ผลลัพธ์ที่สอดคล้องกับ thread1 เท่านั้น

สรุป

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

  • ตัวสร้าง เธรด สามารถใช้เพื่อสร้างอ็อบเจ็กต์เธรด การใช้ threading.Thread(target=<callable>,args=(<tuple of args>)) สร้างเธรดที่รัน เป้าหมาย callable ด้วยอาร์กิวเมนต์ที่ระบุใน args
  • โปรแกรม Python ทำงานบนเธรดหลัก ดังนั้นอ็อบเจ็กต์เธรดที่คุณสร้างจึงเป็นเธรดเพิ่มเติม คุณสามารถเรียกใช้ ฟังก์ชัน active_count() คืนค่าจำนวนเธรดที่ใช้งานอยู่ในอินสแตนซ์ใดก็ได้
  • คุณสามารถเริ่มเธรดโดยใช้เมธอด start() บนออบเจ็กต์เธรดและรอจนกว่าการดำเนินการจะเสร็จสิ้นโดยใช้เมธอด join()

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