การควบคุม Multi-Core Power ด้วย Asyncio ใน Python
นี่เป็นหนึ่งในบทความของฉันภายใต้คอลัมน์ Python Concurrencyและหากคุณพบว่ามีประโยชน์ คุณสามารถอ่านส่วนที่เหลือได้จากที่นี่
การแนะนำ
ในบทความนี้ ฉันจะแสดงวิธีรันโค้ด Python asyncio บน CPU แบบมัลติคอร์เพื่อปลดล็อกประสิทธิภาพทั้งหมดของงานพร้อมกัน
ปัญหาของเราคืออะไร?
asyncio ใช้เพียงหนึ่งคอร์
ในบทความก่อนหน้านี้ ฉันได้กล่าวถึงกลไกของการใช้ Python asyncio อย่างละเอียด ด้วยความรู้นี้ คุณสามารถเรียนรู้ว่า asyncio อนุญาตให้งานที่ผูกกับ IO ดำเนินการด้วยความเร็วสูงโดยการสลับการดำเนินการด้วยตนเองเพื่อข้ามกระบวนการช่วงชิง GIL ระหว่างการสลับงานแบบมัลติเธรด
ในทางทฤษฎี เวลาดำเนินการของงานที่ผูกกับ IO จะขึ้นอยู่กับเวลาตั้งแต่เริ่มต้นจนถึงการตอบสนองของการดำเนินการ IO และไม่ได้ขึ้นอยู่กับประสิทธิภาพของ CPU ของคุณ ดังนั้นเราจึงสามารถเริ่มต้นงาน IO หลายหมื่นงานพร้อมๆ กัน และดำเนินการให้เสร็จสิ้นได้อย่างรวดเร็ว
แต่เมื่อเร็วๆ นี้ ฉันกำลังเขียนโปรแกรมที่ต้องรวบรวมข้อมูลหน้าเว็บหลายหมื่นหน้าพร้อมกัน และพบว่าแม้ว่าโปรแกรม asyncio ของฉันจะมีประสิทธิภาพมากกว่าโปรแกรมที่ใช้การรวบรวมข้อมูลหน้าเว็บซ้ำๆ แต่ก็ยังทำให้ฉันต้องรอเป็นเวลานาน ฉันควรใช้คอมพิวเตอร์เต็มประสิทธิภาพหรือไม่ ดังนั้นฉันจึงเปิดตัวจัดการงานและตรวจสอบ:
ฉันพบว่าตั้งแต่เริ่มต้น โค้ดของฉันทำงานบนแกน CPU เพียงแกนเดียว และแกนอื่นๆ อีกหลายแกนไม่ได้ใช้งาน นอกเหนือจากการเรียกใช้การดำเนินการ IO เพื่อดึงข้อมูลเครือข่ายแล้ว งานจะต้องคลายแพ็กและจัดรูปแบบข้อมูลหลังจากที่ส่งคืน แม้ว่าส่วนนี้ของการดำเนินการจะไม่ใช้ประสิทธิภาพของ CPU มากนัก แต่หลังจากงานจำนวนมากขึ้น การดำเนินการที่เชื่อมโยงกับ CPU เหล่านี้จะส่งผลกระทบต่อประสิทธิภาพโดยรวมอย่างมาก
ฉันต้องการทำให้ asyncio งานพร้อมกันทำงานพร้อมกันในหลายคอร์ นั่นจะบีบประสิทธิภาพออกจากคอมพิวเตอร์ของฉันหรือไม่
หลักการพื้นฐานของ asyncio
ในการไขปริศนานี้ เราต้องเริ่มต้นด้วยการนำ asyncio ไปใช้ นั่นคือ event loop
ดังที่แสดงในรูป การปรับปรุงประสิทธิภาพของ asyncio สำหรับโปรแกรมเริ่มต้นด้วยงานที่ต้องใช้ IO มาก งานที่ต้องใช้ IO มาก ได้แก่ คำขอ HTTP, การอ่านและเขียนไฟล์, การเข้าถึงฐานข้อมูล ฯลฯ คุณสมบัติที่สำคัญที่สุดของงานเหล่านี้คือ CPU ไม่บล็อกและใช้เวลามากในการคำนวณในขณะที่รอข้อมูลภายนอกส่งคืน ซึ่งก็คือ แตกต่างอย่างมากจากงานซิงโครนัสประเภทอื่นที่ต้องใช้ CPU ตลอดเวลาเพื่อคำนวณผลลัพธ์เฉพาะ
เมื่อเราสร้างชุดงาน asyncio โค้ดจะใส่งานเหล่านี้ลงในคิวก่อน ณ จุดนี้ มีเธรดที่เรียกว่า event loop ซึ่งจะดึงทีละงานจากคิวและดำเนินการ เมื่องานมาถึงคำสั่ง wait และรอ (โดยปกติสำหรับการส่งคืนคำขอ) ลูปเหตุการณ์จะคว้างานอื่นจากคิวและดำเนินการ จนกว่างานที่รอก่อนหน้านี้จะได้รับข้อมูลผ่านการเรียกกลับ การวนซ้ำเหตุการณ์จะกลับไปที่งานที่รอก่อนหน้าและดำเนินการโค้ดที่เหลือให้เสร็จสิ้น
เนื่องจากเธรดการวนซ้ำเหตุการณ์ดำเนินการบนคอร์เดียวเท่านั้น การวนซ้ำเหตุการณ์จึงบล็อกเมื่อ "โค้ดที่เหลือ" ใช้เวลา CPU มากเกินไป เมื่องานในหมวดหมู่นี้มีจำนวนมาก แต่ละส่วนการบล็อกขนาดเล็กจะรวมกันและทำให้โปรแกรมโดยรวมทำงานช้าลง
ทางออกของฉันคืออะไร?
จากนี้ เรารู้ว่าโปรแกรม asyncio ทำงานช้าลงเนื่องจากโค้ด Python ของเรารัน event loop บนคอร์เดียวเท่านั้น และการประมวลผลข้อมูล IO ทำให้โปรแกรมทำงานช้าลง มีวิธีเริ่มต้นการวนซ้ำเหตุการณ์ในแต่ละคอร์ CPU เพื่อดำเนินการหรือไม่
อย่างที่เราทราบ เริ่มต้นด้วย Python 3.7 โค้ด asyncio ทั้งหมดแนะนำให้ดำเนินการโดยใช้ method asyncio.run
ซึ่งเป็นนามธรรมระดับสูงที่เรียก event loop เพื่อรันโค้ดแทนโค้ดต่อไปนี้:
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(task())
finally:
loop.close()
บทความก่อนหน้านี้ใช้ตัวอย่างในชีวิตจริงเพื่ออธิบายโดยใช้loop.run_in_executor
วิธีการของ asyncio เพื่อดำเนินการโค้ดแบบขนานในกลุ่มกระบวนการ ในขณะเดียวกันก็รับผลลัพธ์ของกระบวนการย่อยแต่ละรายการจากกระบวนการหลัก หากคุณยังไม่ได้อ่านบทความก่อนหน้านี้ คุณสามารถตรวจสอบได้ที่นี่:
ดังนั้น โซลูชันของเราจึงเกิดขึ้น: กระจายงานพร้อมกันจำนวนมากไปยังหลายกระบวนการย่อยโดยใช้การดำเนินการแบบมัลติคอร์ผ่านเมธอด จากนั้นloop.run_in_executor
เรียกasyncio.run
ใช้กระบวนการย่อยแต่ละรายการเพื่อเริ่มลูปเหตุการณ์ที่เกี่ยวข้องและรันโค้ดพร้อมกัน แผนภาพต่อไปนี้แสดงโฟลว์ทั้งหมด:
โดยที่ส่วนสีเขียวแสดงถึงกระบวนการย่อยที่เราเริ่มต้น ส่วนสีเหลืองแสดงถึงงานที่เราเริ่มต้นพร้อมกัน
การเตรียมตัวก่อนเริ่ม
จำลองการปฏิบัติงาน
ก่อนที่เราจะแก้ปัญหาได้ต้องมีการเตรียมตัวก่อนเริ่ม ในตัวอย่างนี้ เราไม่สามารถเขียนโค้ดจริงเพื่อรวบรวมข้อมูลเนื้อหาเว็บได้ เนื่องจากอาจสร้างความรำคาญให้กับเว็บไซต์เป้าหมาย ดังนั้นเราจะจำลองงานจริงของเราด้วยโค้ด:
ดังที่โค้ดแสดง เราใช้ครั้งแรกasyncio.sleep
เพื่อจำลองการส่งคืนงาน IO ในเวลาสุ่มและผลรวมซ้ำเพื่อจำลองการประมวลผลของ CPU หลังจากข้อมูลถูกส่งกลับ
ผลกระทบของรหัสดั้งเดิม
ต่อไป เราจะใช้วิธีดั้งเดิมในการเริ่มงานพร้อมกัน 10,000 งานในวิธีหลัก และดูเวลาที่ใช้โดยงานพร้อมกันกลุ่มนี้:
ดังที่แสดงในรูปภาพ การดำเนินการงาน asyncio ด้วยคอร์เดียวใช้เวลานานกว่า
การนำโค้ดไปใช้
ต่อไป ให้ใช้โค้ด asyncio แบบมัลติคอร์ตามผังงาน และดูว่าประสิทธิภาพดีขึ้นหรือไม่
การออกแบบโครงสร้างโดยรวมของโค้ด
ขั้นแรก ในฐานะสถาปนิก เรายังจำเป็นต้องกำหนดโครงสร้างสคริปต์โดยรวมก่อนว่าต้องใช้เมธอดใด และงานใดที่แต่ละเมธอดต้องทำให้สำเร็จ:
การดำเนินการเฉพาะของแต่ละวิธี
จากนั้นมาปรับใช้แต่ละวิธีทีละขั้นตอน
เมธอดquery_concurrently
จะเริ่มชุดงานที่ระบุพร้อมกันและรับผลลัพธ์ผ่านasyncio.gather
เมธอด:
เมธอด นี้run_batch_tasks
ไม่ใช่เมธอด async เนื่องจากเริ่มต้นโดยตรงในกระบวนการลูก:
ในที่สุดก็มีmain
วิธีการ ของเรา เมธอดนี้จะเรียกloop.run_in_executor
เมธอดเพื่อให้run_batch_tasks
เมธอดดำเนินการในกลุ่มกระบวนการและรวมผลลัพธ์ของการดำเนินการกระบวนการย่อยลงในรายการ:
เนื่องจากเรากำลังเขียนสคริปต์แบบหลายกระบวนการ เราจำเป็นต้องใช้if __name__ == “__main__”
เพื่อเริ่มเมธอดหลักในกระบวนการหลัก:
รันโค้ดและดูผลลัพธ์
ต่อไป เราจะเริ่มสคริปต์และดูภาระของแต่ละคอร์ในตัวจัดการงาน:
อย่างที่คุณเห็น คอร์ CPU ทั้งหมดถูกใช้
สุดท้าย เราสังเกตเวลาดำเนินการโค้ดและยืนยันว่าโค้ด asyncio แบบมัลติเธรดช่วยเพิ่มความเร็วในการเรียกใช้โค้ดได้หลายเท่า! ภารกิจเสร็จสมบูรณ์!
บทสรุป
ในบทความนี้ ฉันได้อธิบายว่าทำไม asyncio จึงสามารถดำเนินการงานที่ใช้ IO มากพร้อมกันได้ แต่ยังใช้เวลานานกว่าที่คาดไว้เมื่อเรียกใช้งานพร้อมกันจำนวนมาก
เป็นเพราะในโครงร่างการใช้งานแบบดั้งเดิมของรหัส asyncio เหตุการณ์วนรอบสามารถดำเนินการงานบนคอร์เดียวเท่านั้น และคอร์อื่น ๆ อยู่ในสถานะไม่ได้ใช้งาน
ดังนั้นฉันจึงใช้วิธีแก้ปัญหาให้คุณเรียกแต่ละเหตุการณ์วนซ้ำบนหลายคอร์แยกกันเพื่อทำงานพร้อมกันพร้อมกัน และสุดท้าย ปรับปรุงประสิทธิภาพของรหัสอย่างมาก
เนื่องจากข้อจำกัดความสามารถของข้าพเจ้า วิธีแก้ปัญหาในบทความนี้จึงมีความไม่สมบูรณ์อย่างหลีกเลี่ยงไม่ได้ ฉันยินดีรับความคิดเห็นและการสนทนาของคุณ ฉันจะตอบคุณอย่างแข็งขัน