ฉันจะทำ Python loop แบบขนานได้อย่างไร


255

นี่อาจเป็นคำถามที่ไม่สำคัญ แต่ฉันจะขนานวงรอบต่อไปนี้ในไพ ธ อนได้อย่างไร

# setup output lists
output1 = list()
output2 = list()
output3 = list()

for j in range(0, 10):
    # calc individual parameter value
    parameter = j * offset
    # call the calculation
    out1, out2, out3 = calc_stuff(parameter = parameter)

    # put results into correct output list
    output1.append(out1)
    output2.append(out2)
    output3.append(out3)

ฉันรู้วิธีเริ่มต้นเธรดเดี่ยวใน Python แต่ฉันไม่รู้วิธี "รวบรวม" ผลลัพธ์

กระบวนการหลายอย่างก็ใช้ได้เช่นกัน - สิ่งที่ง่ายที่สุดสำหรับกรณีนี้ ฉันใช้ Linux อยู่ในขณะนี้ แต่โค้ดควรทำงานบน Windows และ Mac ด้วย

วิธีที่ง่ายที่สุดในการทำให้ขนานรหัสนี้คืออะไร

คำตอบ:


193

การใช้หลายเธรดใน CPython จะไม่ให้ประสิทธิภาพที่ดีขึ้นสำหรับรหัส pure-Python เนื่องจากการล็อคล่ามทั่วโลก (GIL) ฉันขอแนะนำให้ใช้multiprocessingโมดูลแทน:

pool = multiprocessing.Pool(4)
out1, out2, out3 = zip(*pool.map(calc_stuff, range(0, 10 * offset, offset)))

โปรดทราบว่าสิ่งนี้จะไม่ทำงานในล่ามแบบโต้ตอบ

เพื่อหลีกเลี่ยง FUD ปกติรอบ GIL: จะไม่มีประโยชน์ใด ๆ ในการใช้เธรดสำหรับตัวอย่างนี้ คุณต้องการใช้กระบวนการที่นี่ไม่ใช่เธรดเนื่องจากจะหลีกเลี่ยงปัญหาทั้งหมด


46
เนื่องจากนี่เป็นคำตอบที่เลือกจึงเป็นไปได้ไหมที่จะมีตัวอย่างที่ครอบคลุมมากขึ้น? อะไรคือข้อโต้แย้งของcalc_stuff?
Eduardo Pignatelli

2
@EduardoPignatelli โปรดอ่านเอกสารของmultiprocessingโมดูลเพื่อดูตัวอย่างที่ครอบคลุมมากขึ้น Pool.map()โดยทั่วไปแล้วทำงานได้เหมือนmap()กัน แต่ในแบบคู่ขนาน
Sven Marnach

3
มีวิธีเพิ่มแถบการโหลด tqdm ในโครงสร้างของรหัสนี้หรือไม่? ฉันใช้ tqdm (pool.imap (calc_stuff, ช่วง (0, 10 * ออฟเซต, ออฟเซ็ต))) แต่ฉันไม่ได้รับกราฟิกบาร์โหลดเต็มรูปแบบ
user8188120

@ user8188120 ฉันไม่เคยได้ยินชื่อ tqdm มาก่อนเลยขอโทษด้วยฉันไม่สามารถช่วยได้
Sven Marnach

สำหรับแถบการโหลด tqdm ดูคำถามนี้: stackoverflow.com/questions/41920124/…
โยฮันเนส

67

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

ข้อจำกัดความรับผิดชอบ: ฉันเป็นผู้เขียนต้นฉบับของ joblib


1
ฉันลอง joblib กับ jupyter มันไม่ทำงาน หลังจากการโทรล่าช้าแบบขนานหน้าหยุดทำงาน
Jie

1
สวัสดีฉันมีปัญหาในการใช้ joblib ( stackoverflow.com/questions/52166572/ ...... ) คุณมีเงื่อนงำอะไรบ้างที่อาจเป็นสาเหตุ ขอบคุณมาก ๆ.
Ting Sun

ดูเหมือนว่าบางสิ่งที่ฉันต้องการจะยิง! เป็นไปได้หรือไม่ที่จะใช้กับลูปสองครั้งเช่นสำหรับ i ในช่วง (10): สำหรับ j ในช่วง (20)
CutePoison

51

วิธีที่ง่ายที่สุดในการทำให้ขนานรหัสนี้คืออะไร

ผมชอบconcurrent.futuresเรื่องนี้มีอยู่ใน Python3 ตั้งแต่รุ่น 3.2 - และผ่านทางย้ายกลับไปที่ 2.6 และ 2.7 ในPyPi

คุณสามารถใช้เธรดหรือกระบวนการและใช้อินเตอร์เฟสเดียวกันที่แน่นอน

multiprocessing

วางไว้ในไฟล์ - futuretest.py:

import concurrent.futures
import time, random               # add some random sleep time

offset = 2                        # you don't supply these so
def calc_stuff(parameter=None):   # these are examples.
    sleep_time = random.choice([0, 1, 2, 3, 4, 5])
    time.sleep(sleep_time)
    return parameter / 2, sleep_time, parameter * parameter

def procedure(j):                 # just factoring out the
    parameter = j * offset        # procedure
    # call the calculation
    return calc_stuff(parameter=parameter)

def main():
    output1 = list()
    output2 = list()
    output3 = list()
    start = time.time()           # let's see how long this takes

    # we can swap out ProcessPoolExecutor for ThreadPoolExecutor
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for out1, out2, out3 in executor.map(procedure, range(0, 10)):
            # put results into correct output list
            output1.append(out1)
            output2.append(out2)
            output3.append(out3)
    finish = time.time()
    # these kinds of format strings are only available on Python 3.6:
    # time to upgrade!
    print(f'original inputs: {repr(output1)}')
    print(f'total time to execute {sum(output2)} = sum({repr(output2)})')
    print(f'time saved by parallelizing: {sum(output2) - (finish-start)}')
    print(f'returned in order given: {repr(output3)}')

if __name__ == '__main__':
    main()

และนี่คือผลลัพธ์:

$ python3 -m futuretest
original inputs: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
total time to execute 33 = sum([0, 3, 3, 4, 3, 5, 1, 5, 5, 4])
time saved by parallellizing: 27.68999981880188
returned in order given: [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

multithreading

ตอนนี้เปลี่ยนProcessPoolExecutorเป็นThreadPoolExecutorและรันโมดูลอีกครั้ง:

$ python3 -m futuretest
original inputs: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
total time to execute 19 = sum([0, 2, 3, 5, 2, 0, 0, 3, 3, 1])
time saved by parallellizing: 13.992000102996826
returned in order given: [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

ตอนนี้คุณทำทั้งมัลติเธรดและมัลติโปรเซสเซอร์!

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

การสุ่มตัวอย่างมีขนาดเล็กเกินไปที่จะเปรียบเทียบผลลัพธ์

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

คุณสามารถซ้อนหลายเธรดภายในหลายโพรเซสได้ แต่ไม่แนะนำให้ใช้หลายเธรดในการแยกหลายโพรเซส


ThreadPoolExecutor ข้ามข้อ จำกัด ที่ GIL กำหนดไว้หรือไม่ นอกจากนี้คุณไม่จำเป็นต้องเข้าร่วม () เพื่อรอให้ผู้ดำเนินการให้เสร็จสิ้นหรือสิ่งนี้ได้รับการดูแลโดยปริยายในบริบทผู้จัดการ
PirateApp

1
ไม่และไม่ใช่เพื่อ "จัดการโดยนัย"
Aaron Hall

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

@komodovaran_ กระบวนการเป็นกระบวนการ Python แบบเต็มหนึ่งรายการต่อกระบวนการในขณะที่เธรดเป็นเพียงเธรดของการดำเนินการที่มีสแต็กของตัวเองที่ใช้ร่วมกันในกระบวนการที่ใช้ร่วมกันไบต์ของมันและทุกอย่างอื่นมีอยู่ในหน่วยความจำกับเธรดอื่น ๆ ทั้งหมด - ?
Aaron Hall

49
from joblib import Parallel, delayed
import multiprocessing

inputs = range(10) 
def processInput(i):
    return i * i

num_cores = multiprocessing.cpu_count()

results = Parallel(n_jobs=num_cores)(delayed(processInput)(i) for i in inputs)
print(results)

ข้างต้นทำงานได้อย่างสวยงามบนเครื่องของฉัน (Ubuntu, แพ็กเกจ joblib ได้รับการติดตั้งล่วงหน้า แต่สามารถติดตั้งผ่านpip install joblib)

นำมาจากhttps://blog.dominodatalab.com/simple-parallelization/


3
ฉันลองใช้รหัสของคุณ แต่ในระบบของฉันรุ่นต่อเนื่องของรหัสนี้ใช้เวลาประมาณครึ่งนาทีและรุ่นขนานด้านบนใช้เวลา 4 นาที ทำไมเป็นเช่นนั้น
shaifali Gupta

3
ขอบคุณสำหรับคำตอบ! ฉันคิดว่านี่เป็นวิธีที่หรูหราที่สุดในการทำเช่นนี้ในปี 2019
Heikki Pulkkinen

2
การประมวลผลหลายตัวไม่ถูกต้องสำหรับ Python 3.x ดังนั้นนี่ไม่ได้ผลสำหรับฉัน
EngrStudent

2
@EngrStudent ไม่แน่ใจว่าสิ่งที่คุณหมายถึงโดย "ไม่ถูกต้อง" มันใช้งานได้กับ Python 3.6.x สำหรับฉัน
tyrex

@tyrex ขอบคุณสำหรับการแบ่งปัน! แพ็กเกจ joblib นี้ยอดเยี่ยมและตัวอย่างใช้ได้สำหรับฉัน แม้ว่าในบริบทที่ซับซ้อนมากขึ้นฉันมีข้อผิดพลาดอย่างน่าเสียดาย github.com/joblib/joblib/issues/949
โบรกเกอร์อาหารเปิด

13

มีข้อดีหลายประการในการใช้Ray :

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

ในกรณีของคุณคุณสามารถเริ่มต้นเรย์และกำหนดฟังก์ชั่นระยะไกล

import ray

ray.init()

@ray.remote(num_return_vals=3)
def calc_stuff(parameter=None):
    # Do something.
    return 1, 2, 3

แล้วเรียกมันขนานกัน

output1, output2, output3 = [], [], []

# Launch the tasks.
for j in range(10):
    id1, id2, id3 = calc_stuff.remote(parameter=j)
    output1.append(id1)
    output2.append(id2)
    output3.append(id3)

# Block until the results have finished and get the results.
output1 = ray.get(output1)
output2 = ray.get(output2)
output3 = ray.get(output3)

ในการรันตัวอย่างเดียวกันบนคลัสเตอร์บรรทัดเดียวที่จะเปลี่ยนคือการเรียกไปยัง ray.init () เอกสารที่เกี่ยวข้องสามารถพบได้ที่นี่

โปรดทราบว่าฉันกำลังช่วยพัฒนาเรย์


1
สำหรับใครที่กำลังพิจารณา ray มันอาจจะเกี่ยวข้องถ้ารู้ว่ามันไม่สนับสนุน Windows แฮ็กบางอย่างที่จะทำให้มันทำงานใน Windows โดยใช้ WSL (Windows Subsystem สำหรับ Linux) เป็นไปได้แม้ว่ามันจะไม่ค่อยอยู่ในกรอบถ้าคุณต้องการใช้ Windows
OscarVanL

9

นี่เป็นวิธีที่ง่ายที่สุดที่จะทำ!

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

import asyncio

def background(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, **kwargs)

    return wrapped

@background
def your_function(argument):
    #code

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

@background
def your_function(argument):
    time.sleep(5)
    print('function finished for '+str(argument))


for i in range(10):
    your_function(i)


print('loop finished')

สิ่งนี้สร้างผลลัพธ์ต่อไปนี้:

loop finished
function finished for 4
function finished for 8
function finished for 0
function finished for 3
function finished for 6
function finished for 2
function finished for 5
function finished for 7
function finished for 9
function finished for 1

ฉันคิดว่ามีการพิมพ์ผิดwrapped()และควร**kwargsแทนที่*kwargs
jakub-olczyk

อ๊ะ! ความผิดพลาดของฉัน. แก้ไข!
User5

6

ทำไมคุณไม่ใช้เธรดและอีกหนึ่ง mutex เพื่อป้องกันรายการส่วนกลาง

import os
import re
import time
import sys
import thread

from threading import Thread

class thread_it(Thread):
    def __init__ (self,param):
        Thread.__init__(self)
        self.param = param
    def run(self):
        mutex.acquire()
        output.append(calc_stuff(self.param))
        mutex.release()   


threads = []
output = []
mutex = thread.allocate_lock()

for j in range(0, 10):
    current = thread_it(j * offset)
    threads.append(current)
    current.start()

for t in threads:
    t.join()

#here you have output list filled with data

โปรดทราบว่าคุณจะเร็วเท่ากับเธรดที่ช้าที่สุดของคุณ


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

3
@skrgwasme ฉันรู้ว่าคุณรู้เรื่องนี้ แต่เมื่อคุณใช้คำว่า "พวกเขาจะไม่ขนานอะไร" ซึ่งอาจทำให้ผู้อ่านเข้าใจผิด หากการดำเนินการใช้เวลานานเนื่องจาก IO ถูกผูกไว้หรือสลีปในขณะที่รอเหตุการณ์ผู้แปลจะได้รับอิสระในการรันเธรดอื่นดังนั้นสิ่งนี้จะส่งผลให้ความเร็วเพิ่มขึ้นที่ผู้คนคาดหวังในกรณีเหล่านั้น เฉพาะเธรดที่เชื่อมโยงกับ CPU เท่านั้นที่ได้รับผลกระทบจากสิ่งที่ skrrgwasme พูด
Jonathan Hartley

5

ฉันพบว่าjoblibมีประโยชน์มากกับฉัน โปรดดูตัวอย่างต่อไปนี้:

from joblib import Parallel, delayed
def yourfunction(k):   
    s=3.14*k*k
    print "Area of a circle with a radius ", k, " is:", s

element_run = Parallel(n_jobs=-1)(delayed(yourfunction)(k) for k in range(1,10))

n_jobs = -1: ใช้แกนที่มีอยู่ทั้งหมด


14
คุณจะรู้ว่ามันเป็นการดีกว่าที่จะตรวจสอบคำตอบที่มีอยู่แล้วก่อนโพสต์ของคุณเอง คำตอบนี้joblibยังเสนอที่จะใช้
sanyash

2

สมมติว่าเรามีฟังก์ชั่น async

async def work_async(self, student_name: str, code: str, loop):
"""
Some async function
"""
    # Do some async procesing    

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

async def process_students(self, student_name: str, loop):
    market = sys.argv[2]
    subjects = [...] #Some large array
    batchsize = 5
    for i in range(0, len(subjects), batchsize):
        batch = subjects[i:i+batchsize]
        await asyncio.gather(*(self.work_async(student_name,
                                           sub['Code'],
                                           loop)
                       for sub in batch))

1

ดูที่นี้

http://docs.python.org/library/queue.html

นี่อาจไม่ใช่วิธีที่ถูกต้องที่จะทำ แต่ฉันจะทำสิ่งที่ชอบ;

รหัสจริง

from multiprocessing import Process, JoinableQueue as Queue 

class CustomWorker(Process):
    def __init__(self,workQueue, out1,out2,out3):
        Process.__init__(self)
        self.input=workQueue
        self.out1=out1
        self.out2=out2
        self.out3=out3
    def run(self):
            while True:
                try:
                    value = self.input.get()
                    #value modifier
                    temp1,temp2,temp3 = self.calc_stuff(value)
                    self.out1.put(temp1)
                    self.out2.put(temp2)
                    self.out3.put(temp3)
                    self.input.task_done()
                except Queue.Empty:
                    return
                   #Catch things better here
    def calc_stuff(self,param):
        out1 = param * 2
        out2 = param * 4
        out3 = param * 8
        return out1,out2,out3
def Main():
    inputQueue = Queue()
    for i in range(10):
        inputQueue.put(i)
    out1 = Queue()
    out2 = Queue()
    out3 = Queue()
    processes = []
    for x in range(2):
          p = CustomWorker(inputQueue,out1,out2,out3)
          p.daemon = True
          p.start()
          processes.append(p)
    inputQueue.join()
    while(not out1.empty()):
        print out1.get()
        print out2.get()
        print out3.get()
if __name__ == '__main__':
    Main()

หวังว่าจะช่วย


1

สิ่งนี้อาจมีประโยชน์เมื่อใช้การประมวลผลหลายตัวและการคำนวณแบบขนาน / แบบกระจายใน Python

บทแนะนำ YouTube เกี่ยวกับการใช้แพ็คเกจเทคโนโลยี

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

techila.peach(funcname = 'theheavyalgorithm', # Function that will be called on the compute nodes/ Workers
    files = 'theheavyalgorithm.py', # Python-file that will be sourced on Workers
    jobs = jobcount # Number of Jobs in the Project
    )

1
ในขณะที่ลิงค์นี้อาจตอบคำถามได้ดีกว่าที่จะรวมส่วนสำคัญของคำตอบที่นี่และให้ลิงค์สำหรับการอ้างอิง คำตอบสำหรับลิงค์เท่านั้นอาจไม่ถูกต้องหากหน้าเว็บที่เชื่อมโยงมีการเปลี่ยนแปลง
SL Barth - Reinstate Monica

2
@SLBarth ขอบคุณสำหรับความคิดเห็น ฉันเพิ่มโค้ดตัวอย่างขนาดเล็กลงในคำตอบ
TEE

1

ขอบคุณ @iuryxavier

from multiprocessing import Pool
from multiprocessing import cpu_count


def add_1(x):
    return x + 1

if __name__ == "__main__":
    pool = Pool(cpu_count())
    results = pool.map(add_1, range(10**12))
    pool.close()  # 'TERM'
    pool.join()   # 'KILL'

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

-1

ตัวอย่างง่ายๆของการประมวลผลแบบขนานคือ

from multiprocessing import Process

output1 = list()
output2 = list()
output3 = list()

def yourfunction():
    for j in range(0, 10):
        # calc individual parameter value
        parameter = j * offset
        # call the calculation
        out1, out2, out3 = calc_stuff(parameter=parameter)

        # put results into correct output list
        output1.append(out1)
        output2.append(out2)
        output3.append(out3)

if __name__ == '__main__':
    p = Process(target=pa.yourfunction, args=('bob',))
    p.start()
    p.join()

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