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

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

ใน CPU ที่มีแกน N กระบวนการ N สามารถดำเนินการพร้อมกันได้ในเวลาเดียวกัน อย่างไรก็ตาม สองเธรดของกระบวนการเดียวกันไม่สามารถดำเนินการแบบขนานได้ แต่สามารถดำเนินการพร้อมกันได้ เราจะพูดถึงแนวคิดเรื่องการทำงานพร้อมกันกับความเท่าเทียมในหัวข้อถัดไป
จากสิ่งที่เราได้เรียนรู้ไปแล้ว มาสรุปความแตกต่างระหว่างกระบวนการและเธรด
ลักษณะเฉพาะ | กระบวนการ | เกลียว |
หน่วยความจำ | หน่วยความจำเฉพาะ | หน่วยความจำที่ใช้ร่วมกัน |
โหมดการดำเนินการ | ขนานกัน | พร้อมกัน; แต่ไม่ขนานกัน |
ดำเนินการโดย | ระบบปฏิบัติการ | ล่าม CPython |
มัลติเธรดใน Python
ใน Python Global Interpreter Lock (GIL) ช่วยให้มั่นใจได้ว่า มีเพียง เธรดเดียวเท่านั้นที่สามารถรับการล็อกและเรียกใช้ได้ตลอดเวลา เธรดทั้งหมดควรได้รับล็อกนี้เพื่อเรียกใช้ สิ่งนี้ทำให้มั่นใจได้ว่ามีเพียงเธรดเดียวเท่านั้นที่สามารถดำเนินการได้— ณ เวลาใดก็ตาม—และหลีกเลี่ยงการทำมัลติเธรด พร้อมกัน
ตัวอย่างเช่น พิจารณาสองเธรด t1
และ t2
ของกระบวนการเดียวกัน เนื่องจากเธรดใช้ข้อมูลเดียวกันเมื่อ t1
กำลังอ่านค่าเฉพาะ k
ดังนั้น t2
อาจแก้ไขค่า k
เดียวกัน นี้สามารถนำไปสู่ภาวะชะงักงันและผลลัพธ์ที่ไม่พึงประสงค์ แต่มีเพียงหนึ่งเธรดเท่านั้นที่สามารถรับการล็อกและเรียกใช้ได้ทุกกรณี ดังนั้น GIL จึงรับประกัน ความปลอดภัยของเกลียว
แล้วเราจะบรรลุความสามารถในการทำมัลติเธรดใน Python ได้อย่างไร เพื่อให้เข้าใจสิ่งนี้ เรามาพูดถึงแนวคิดเรื่องการทำงานพร้อมกันและความเท่าเทียมกัน
Concurrency vs. 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
ดำเนินการเสร็จสิ้น คุณสามารถเรียกใช้เมธอด 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 ที่จะเกิดขึ้นของคุณ มีความสุขในการเข้ารหัส!