อะไรคือความแตกต่างระหว่างโมดูลเธรดและโมดูลหลายกระบวนการ?


146

ฉันกำลังเรียนรู้วิธีใช้threadingและmultiprocessingโมดูลใน Python เพื่อรันการดำเนินการบางอย่างพร้อมกันและเร่งความเร็วโค้ด

ฉันพบว่าสิ่งนี้ยาก (อาจเป็นเพราะฉันไม่มีพื้นฐานทางทฤษฎีเกี่ยวกับเรื่องนี้) ที่จะเข้าใจว่าความแตกต่างระหว่างthreading.Thread()วัตถุกับวัตถุนั้นคือmultiprocessing.Process()อะไร

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

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

ดังนั้นฉันควรใช้โมดูลthreadingและเมื่อmultiprocessingใด

คุณสามารถเชื่อมโยงฉันกับแหล่งข้อมูลบางส่วนที่อธิบายแนวคิดเบื้องหลังโมดูลทั้งสองนี้และวิธีใช้อย่างเหมาะสมสำหรับงานที่ซับซ้อนได้หรือไม่


นอกจากนี้ยังมีThreadโมดูล (เรียก_threadใน python 3.x) พูดตามตรงฉันไม่เคยเข้าใจความแตกต่างด้วยตัวเองเลย ...
Dunno

3
@ ดันโน: ตามที่Thread/ _threadเอกสารระบุไว้อย่างชัดเจนว่าเป็น "ดั้งเดิมระดับต่ำ" คุณอาจใช้มันเพื่อวัตถุประสานสร้างที่กำหนดเองเพื่อควบคุมการเข้าร่วมการสั่งซื้อของต้นไม้ของกระทู้, ฯลฯ threadingถ้าคุณไม่สามารถนึกว่าทำไมคุณจะต้องใช้มันไม่ได้ใช้มันและติดกับ
abarnert

คำตอบ:


268

อะไร Giulio ฝรั่งเศสกล่าวว่าเป็นจริงสำหรับ multithreading กับ multiprocessing ทั่วไป

อย่างไรก็ตาม Python *มีปัญหาเพิ่มเติม: มี Global Interpreter Lock ที่ป้องกันสองเธรดในกระบวนการเดียวกันจากการเรียกใช้โค้ด Python ในเวลาเดียวกัน ซึ่งหมายความว่าหากคุณมี 8 คอร์และเปลี่ยนรหัสของคุณเพื่อใช้ 8 เธรดจะไม่สามารถใช้ CPU 800% และทำงานได้เร็วขึ้น 8 เท่า จะใช้ CPU 100% เดียวกันและทำงานด้วยความเร็วเท่ากัน (ในความเป็นจริงมันจะทำงานช้าลงเล็กน้อยเนื่องจากมีค่าใช้จ่ายเพิ่มเติมจากการทำเธรดแม้ว่าคุณจะไม่มีข้อมูลที่แชร์ก็ตาม แต่ก็ไม่ต้องสนใจสิ่งนั้นในตอนนี้)

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

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

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

การใช้กระบวนการแยกกันไม่มีปัญหาดังกล่าวกับ GIL เนื่องจากแต่ละกระบวนการมี GIL แยกกัน แน่นอนว่าคุณยังคงมีข้อแลกเปลี่ยนระหว่างเธรดและโปรเซสเหมือนกันกับภาษาอื่น ๆ - การแชร์ข้อมูลระหว่างโปรเซสนั้นยากและแพงกว่าระหว่างเธรดการรันโปรเซสจำนวนมากหรือการสร้างและทำลายอาจมีค่าใช้จ่ายสูง บ่อยครั้ง ฯลฯ แต่ GIL ให้ความสำคัญกับความสมดุลของกระบวนการในแบบที่ไม่เป็นความจริงเช่น C หรือ Java ดังนั้นคุณจะพบว่าตัวเองใช้การประมวลผลหลายขั้นตอนใน Python บ่อยกว่าที่คุณใช้ใน C หรือ Java


ในขณะเดียวกันปรัชญา "รวมแบตเตอรี่" ของ Python นำเสนอข่าวดี: การเขียนโค้ดเป็นเรื่องง่ายมากที่สามารถสลับไปมาระหว่างเธรดและกระบวนการต่างๆได้ด้วยการเปลี่ยนแบบซับเดียว

หากคุณออกแบบโค้ดของคุณในรูปแบบของ "งาน" ที่มีอยู่ในตัวซึ่งจะไม่แชร์สิ่งใดกับงานอื่น ๆ (หรือโปรแกรมหลัก) ยกเว้นอินพุตและเอาต์พุตคุณสามารถใช้concurrent.futuresไลบรารีเพื่อเขียนโค้ดของคุณรอบเธรดพูลดังนี้:

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    executor.submit(job, argument)
    executor.map(some_function, collection_of_independent_things)
    # ...

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

ตอนนี้หากปรากฎว่าโปรแกรมของคุณใช้ CPU 100% อยู่ตลอดเวลาและการเพิ่มเธรดมากขึ้นทำให้ช้าลงแสดงว่าคุณกำลังประสบปัญหา GIL ดังนั้นคุณต้องเปลี่ยนไปใช้กระบวนการ สิ่งที่คุณต้องทำคือเปลี่ยนบรรทัดแรก:

with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:

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


แต่ถ้างานของคุณไม่สามารถอยู่ได้ด้วยตัวเองล่ะ? หากคุณสามารถออกแบบโค้ดของคุณในแง่ของงานที่ส่งผ่านข้อความจากที่หนึ่งไปยังอีกที่หนึ่งได้ก็ยังค่อนข้างง่าย คุณอาจต้องใช้threading.Threadหรือmultiprocessing.Processแทนที่จะพึ่งพาสระว่ายน้ำ และคุณจะต้องสร้างqueue.Queueหรือmultiprocessing.Queueวัตถุอย่างชัดเจน (มีตัวเลือกอื่น ๆ อีกมากมายเช่นไปป์ซ็อกเก็ตไฟล์ที่มีฝูง ... แต่ประเด็นคือคุณต้องทำบางอย่างด้วยตนเองหากเวทมนตร์อัตโนมัติของ Executor ไม่เพียงพอ)

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


จากความคิดเห็นคุณต้องการทราบความแตกต่างระหว่างเธรดและกระบวนการใน Python จริงๆแล้วถ้าคุณอ่านคำตอบของ Giulio Franco และของฉันและลิงก์ทั้งหมดของเรามันควรจะครอบคลุมทุกอย่าง ... แต่บทสรุปจะมีประโยชน์อย่างแน่นอนดังนั้นต่อไปนี้:

  1. เธรดแชร์ข้อมูลตามค่าเริ่มต้น กระบวนการทำไม่ได้
  2. อันเป็นผลมาจาก (1) การส่งข้อมูลระหว่างกระบวนการโดยทั่วไปจำเป็นต้องมีการดองและการแกะออก **
  3. ผลที่ตามมาอีกประการหนึ่งของ (1) การแบ่งปันข้อมูลโดยตรงระหว่างกระบวนการโดยทั่วไปต้องวางไว้ในรูปแบบระดับต่ำเช่น Value, Array และctypesประเภท
  4. กระบวนการต่างๆไม่อยู่ภายใต้ GIL
  5. ในบางแพลตฟอร์ม (ส่วนใหญ่เป็น Windows) กระบวนการสร้างและทำลายมีราคาแพงกว่ามาก
  6. มีข้อ จำกัด พิเศษบางอย่างเกี่ยวกับกระบวนการซึ่งบางอย่างก็แตกต่างกันไปในแต่ละแพลตฟอร์ม ดูหลักเกณฑ์การเขียนโปรแกรมสำหรับรายละเอียด
  7. threadingโมดูลไม่ได้มีบางส่วนของคุณสมบัติของmultiprocessingโมดูล (คุณสามารถใช้multiprocessing.dummyเพื่อรับ API ส่วนใหญ่ที่ขาดหายไปที่ด้านบนของเธรดหรือคุณสามารถใช้โมดูลระดับที่สูงขึ้นได้เช่นconcurrent.futuresและไม่ต้องกังวลกับมัน)

* ไม่ใช่ Python จริงภาษาที่มีปัญหานี้ แต่เป็น CPython ซึ่งเป็นการใช้งาน "มาตรฐาน" ของภาษานั้น การใช้งานอื่น ๆ บางอย่างไม่มี GIL เช่น Jython

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


ขอบคุณ แต่ฉันไม่แน่ใจว่าฉันเข้าใจทุกอย่าง อย่างไรก็ตามฉันพยายามทำเล็กน้อยเพื่อจุดประสงค์ในการเรียนรู้และเล็กน้อยเนื่องจากการใช้เธรดที่ไร้เดียงสาฉันจึงลดความเร็วของโค้ดลงครึ่งหนึ่ง (เริ่มมากกว่า 1,000 เธรดในเวลาเดียวกันแต่ละครั้งเรียกแอปภายนอก .. นี้อิ่มตัว cpu แต่มีความเร็วเพิ่มขึ้น x2) ฉันคิดว่าการจัดการเธรดอย่างชาญฉลาดอาจช่วยเพิ่มความเร็วของโค้ดได้จริงๆ ..
lucacerone

3
@LucaCerone: อ่าถ้าโค้ดของคุณใช้เวลาส่วนใหญ่ไปกับการรอโปรแกรมภายนอกใช่มันจะได้รับประโยชน์จากเธรด จุดดี. ให้ฉันแก้ไขคำตอบเพื่ออธิบายว่า
abarnert

3
@LucaCerone: ส่วนไหนที่คุณไม่เข้าใจ? หากไม่ทราบระดับความรู้ที่คุณเริ่มต้นก็ยากที่จะเขียนคำตอบที่ดี ... แต่ด้วยข้อเสนอแนะบางประการบางทีเราอาจคิดหาสิ่งที่เป็นประโยชน์สำหรับคุณและผู้อ่านในอนาคตได้เช่นกัน
abarnert

3
@LucaCerone คุณควรอ่าน PEP สำหรับ multiprocessing ที่นี่ จะให้การกำหนดเวลาและตัวอย่างของเธรดเทียบกับการประมวลผลหลายขั้นตอน
mr2ert

1
@LucaCerone: หากอ็อบเจ็กต์ที่เมธอดถูกผูกไว้ไม่มีสถานะที่ซับซ้อนวิธีแก้ปัญหาที่ง่ายที่สุดสำหรับปัญหาการดองคือการเขียนฟังก์ชัน Wrapper แบบโง่ ๆ ที่สร้างอ็อบเจ็กต์และเรียกใช้เมธอด ถ้ามันไม่ได้มีสถานะที่ซับซ้อนแล้วคุณอาจจะต้องทำให้มัน picklable (ซึ่งเป็นเรื่องง่ายสวยนั้นpickleเอกสารอธิบายมัน) def wrapper(obj, *args): return obj.wrapper(*args)แล้วที่เลวร้ายที่สุดเสื้อคลุมโง่ของคุณ
abarnert

33

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

หากคุณต้องการขนานการคำนวณคุณอาจต้องใช้มัลติเธรดเนื่องจากคุณอาจต้องการให้เธรดทำงานร่วมกันในหน่วยความจำเดียวกัน

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


12
ข้อมูลนี้ขาดข้อมูลที่สำคัญบางอย่างเกี่ยวกับ GIL ซึ่งทำให้เข้าใจผิด
abarnert

1
@ mr2ert: ใช่นั่นเป็นข้อมูลที่สำคัญมากโดยสรุป :) แต่มันซับซ้อนกว่านั้นเล็กน้อยฉันจึงเขียนคำตอบแยกต่างหาก
abarnert

2
ฉันคิดว่าฉันแสดงความคิดเห็นโดยบอกว่า @abarnert ถูกต้องและฉันลืมเกี่ยวกับ GIL ในการตอบที่นี่ ดังนั้นคำตอบนี้ไม่ถูกต้องคุณไม่ควรโหวตให้คะแนน
Giulio Franco

6
ฉันลงคะแนนคำตอบนี้เนื่องจากยังไม่ได้คำตอบเลยว่าความแตกต่างระหว่าง Python threadingและmultiprocessing.
Antti Haapala

ฉันได้อ่านพบว่ามี GIL สำหรับทุกกระบวนการ แต่กระบวนการทั้งหมดใช้ตัวแปล python ตัวเดียวกันหรือมีล่ามแยกต่อเธรดหรือไม่?
ตัวแปร

4

ฉันเชื่อว่าลิงก์นี้ตอบคำถามของคุณได้อย่างดี

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


4

คำพูดเอกสาร Python

ฉันได้เน้นคำพูดของเอกสาร Python ที่สำคัญเกี่ยวกับ Process vs Threads และ GIL ไว้ที่: global interpreter lock (GIL) ใน CPython คืออะไร?

กระบวนการเทียบกับการทดลองเธรด

ฉันทำการเปรียบเทียบเล็กน้อยเพื่อแสดงความแตกต่างอย่างเป็นรูปธรรมมากขึ้น

ในเกณฑ์มาตรฐานฉันกำหนดเวลาให้ CPU และ IO ทำงานร่วมกับเธรดจำนวนต่างๆบนCPU ไฮเปอร์เธรด 8ตัว งานที่ให้มาต่อเธรดจะเหมือนกันเสมอดังนั้นจำนวนเธรดที่มากขึ้นหมายถึงปริมาณงานทั้งหมดที่ให้มามากขึ้น

ผลลัพธ์คือ:

ป้อนคำอธิบายภาพที่นี่

ข้อมูลที่พล็อต

สรุป:

  • สำหรับการทำงานที่ถูกผูกไว้ของ CPU การประมวลผลหลายขั้นตอนจะเร็วกว่าเสมอซึ่งน่าจะเกิดจาก GIL

  • สำหรับงานผูกมัด IO ทั้งสองมีความเร็วเท่ากันทุกประการ

  • เธรดจะปรับขนาดได้สูงสุดประมาณ 4x แทนที่จะเป็น 8x ที่คาดไว้เนื่องจากฉันใช้เครื่องไฮเปอร์เธรด 8 ตัว

    ตรงกันข้ามกับการทำงานที่เชื่อมต่อกับ CPU C POSIX ซึ่งถึงความเร็ว 8x ที่คาดไว้: 'ของจริง', 'ผู้ใช้' และ 'sys' หมายถึงอะไรในผลลัพธ์ของเวลา (1)?

    สิ่งที่ต้องทำ: ฉันไม่รู้เหตุผลนี้ต้องมีความไร้ประสิทธิภาพของ Python อื่น ๆ เข้ามาเล่น

รหัสทดสอบ:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub ต้นน้ำ + รหัสพล็อตในไดเรกทอรีเดียวกัน

ทดสอบบน Ubuntu 18.10, Python 3.6.7 ในแล็ปท็อป Lenovo ThinkPad P51 พร้อม CPU: CPU Intel Core i7-7820HQ (4 คอร์ / 8 เธรด), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3,000 MB / s)

เห็นภาพว่าเธรดใดกำลังทำงานในช่วงเวลาที่กำหนด

โพสต์นี้https://rohanvarma.me/GIL/สอนผมว่าคุณสามารถเรียกใช้โทรกลับเมื่อใดก็ตามที่หัวข้อที่กำหนดไว้กับtarget=ข้อโต้แย้งของthreading.Threadmultiprocessing.Processและเหมือนกันสำหรับ

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

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

ซึ่งจะแสดงให้เห็นว่า:

  • เธรดถูกทำให้เป็นอนุกรมโดย GIL
  • กระบวนการทำงานแบบขนานได้

1

นี่คือข้อมูลประสิทธิภาพบางส่วนสำหรับ python 2.6.x ที่เรียกร้องให้ตั้งคำถามกับแนวคิดที่ว่าการเธรดมีประสิทธิภาพมากกว่าการประมวลผลหลายขั้นตอนในสถานการณ์ที่เชื่อมโยงกับ IO ผลลัพธ์เหล่านี้มาจาก 40-processor IBM System x3650 M4 BD

การประมวลผลแบบ IO-Bound: Process Pool ทำงานได้ดีกว่า Thread Pool

>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms

>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms

CPU-Bound Processing: Process Pool ทำงานได้ดีกว่า Thread Pool

>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms

>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms

การทดสอบเหล่านี้ไม่ใช่การทดสอบที่เข้มงวด แต่พวกเขาบอกฉันว่าการประมวลผลหลายขั้นตอนไม่ได้มีประสิทธิภาพทั้งหมดเมื่อเทียบกับเธรด

รหัสที่ใช้ในคอนโซล python แบบโต้ตอบสำหรับการทดสอบข้างต้น

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob

text_for_test = str(range(1,100000))

def fileio(i):
 try :
  os.remove(glob('./test/test-*'))
 except : 
  pass
 f=open('./test/test-'+str(i),'a')
 f.write(text_for_test)
 f.close()
 f=open('./test/test-'+str(i),'r')
 text = f.read()
 f.close()


def square(i):
 return i*i

def timing(f):
 def wrap(*args):
  time1 = time.time()
  ret = f(*args)
  time2 = time.time()
  print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
  return ret
 return wrap

result = None

@timing
def do_work(process_count, items, process_type, method) :
 pool = None
 if process_type == 'process' :
  pool = Pool(processes=process_count)
 else :
  pool = ThreadPool(processes=process_count)
 if method == 'square' : 
  multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]
 else :
  multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]


do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')

do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')

ฉันใช้รหัสของคุณแล้ว (ลบส่วนglobออก) และพบผลลัพธ์ที่น่าสนใจนี้ด้วย Python 2.6.6:>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms >>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms >>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms >>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Alan Garrido

-5

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

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()

def producer(fill_count, empty_count, buffer):
    while True:
        item = produceItem()
        empty_count.down();
        buffer.push(item)
        fill_count.up()

def consumer(fill_count, empty_count, buffer):
    while True:
        fill_count.down()
        item = buffer.pop()
        empty_count.up()
        consume_item(item)

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการซิงโครไนซ์เบื้องต้นได้จาก:

 http://linux.die.net/man/7/sem_overview
 http://docs.python.org/2/library/threading.html

pseudocode อยู่ด้านบน ฉันคิดว่าคุณควรค้นหาปัญหาผู้ผลิต - ผู้บริโภคเพื่อรับข้อมูลอ้างอิงเพิ่มเติม


ขอโทษ Innosam แต่ดูเหมือนว่า C ++ สำหรับฉัน ขอบคุณสำหรับลิงค์ :)
lucacerone

ที่จริงแล้วแนวคิดเบื้องหลังการประมวลผลหลายขั้นตอนและมัลติเธรดนั้นไม่ขึ้นกับภาษา การแก้ปัญหาจะคล้ายกับรหัสด้านบน
innosam

2
นี่ไม่ใช่ C ++; เป็นรหัสเทียม (หรือเป็นรหัสสำหรับภาษาที่พิมพ์แบบไดนามิกเป็นส่วนใหญ่ด้วยไวยากรณ์คล้าย C ที่กล่าวมาฉันคิดว่าการเขียนรหัสเทียมแบบ Python เพื่อสอนผู้ใช้ Python มีประโยชน์มากกว่า (โดยเฉพาะอย่างยิ่งเนื่องจาก psuedocode ที่เหมือน Python มักจะ กลายเป็นโค้ดที่รันได้หรืออย่างน้อยก็ใกล้เคียงซึ่งแทบไม่เป็นความจริงสำหรับรหัสเทียมแบบ C …)
ยกเลิก

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

นอกจากนี้เป็นที่น่าสังเกตว่า Python stdlib มีคิวที่ซิงโครไนซ์อยู่ในตัวซึ่งจะรวมรายละเอียดทั้งหมดเหล่านี้รวมทั้งเธรดและพูลกระบวนการ API ที่เป็นนามธรรมมากยิ่งขึ้น เป็นเรื่องที่ควรค่าแก่การทำความเข้าใจว่าคิวที่ซิงโครไนซ์ทำงานอย่างไรภายใต้ฝาครอบ แต่คุณแทบไม่จำเป็นต้องเขียนด้วยตัวเอง
ยกเลิก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.