Come posso parallelizzare un semplice loop Python?


256

Questa è probabilmente una domanda banale, ma come posso parallelizzare il seguente ciclo in Python?

# 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)

So come avviare singoli thread in Python ma non so come "raccogliere" i risultati.

Anche i processi multipli andrebbero bene, qualunque sia la cosa più semplice per questo caso. Attualmente sto usando Linux ma il codice dovrebbe funzionare anche su Windows e Mac.

Qual è il modo più semplice per parallelizzare questo codice?

Risposte:


193

L'uso di più thread su CPython non offre prestazioni migliori per il codice Python puro grazie al blocco dell'interprete globale (GIL). Suggerisco invece di utilizzare il multiprocessingmodulo:

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

Si noti che questo non funzionerà nell'interprete interattivo.

Per evitare il solito FUD attorno al GIL: non ci sarebbe comunque alcun vantaggio nell'usare i thread per questo esempio. Si desidera utilizzare i processi di qui, non le discussioni, perché evitano un sacco di problemi.


46
Poiché questa è la risposta scelta, è possibile avere un esempio più completo? Quali sono gli argomenti di calc_stuff?
Eduardo Pignatelli,

2
@EduardoPignatelli Ti preghiamo di leggere la documentazione del multiprocessingmodulo per esempi più completi. Pool.map()fondamentalmente funziona come map(), ma in parallelo.
Sven Marnach,

3
C'è un modo per aggiungere semplicemente una barra di caricamento tqdm a questa struttura di codice? Ho usato tqdm (pool.imap (calc_stuff, range (0, 10 * offset, offset))) ma non visualizzo un grafico a barre di caricamento completo.
user8188120

@ user8188120 Non ho mai sentito parlare di tqdm prima, quindi mi dispiace, non posso farci niente.
Sven Marnach,

Per una barra di caricamento tqdm, consultare questa domanda: stackoverflow.com/questions/41920124/…
Johannes

67

Per parallelizzare un semplice ciclo, joblib apporta molto valore all'utilizzo grezzo del multiprocessing. Non solo la breve sintassi, ma anche cose come il raggruppamento trasparente di iterazioni quando sono molto veloci (per rimuovere l'overhead) o l'acquisizione del traceback del processo figlio, per avere una migliore segnalazione degli errori.

Disclaimer: sono l'autore originale di joblib.


1
Ho provato joblib con jupyter, non funziona. Dopo la chiamata ritardata in parallelo, la pagina ha smesso di funzionare.
Jie,

1
Ciao, ho un problema con joblib ( stackoverflow.com/questions/52166572/... ), hai qualche idea di cosa può essere la causa? Grazie mille.
Ting Sun,

Sembra qualcosa che voglio provare! È possibile usarlo con un doppio loop ad es. Per i nel range (10): per j nel range (20)
CutePoison

51

Qual è il modo più semplice per parallelizzare questo codice?

Mi piace molto concurrent.futuresper questo, disponibile in Python3 dalla versione 3.2 - e tramite backport a 2.6 e 2.7 in poi PyPi .

È possibile utilizzare thread o processi e utilizzare esattamente la stessa interfaccia.

multiprocessing

Metti questo in un file - 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()

Ed ecco l'output:

$ 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

Ora ProcessPoolExecutorpassa a ThreadPoolExecutored esegui di nuovo il modulo:

$ 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]

Ora hai fatto sia il multithreading che il multiprocessing!

Nota sulle prestazioni e sull'uso di entrambi insieme.

Il campionamento è troppo piccolo per confrontare i risultati.

Tuttavia, sospetto che il multithreading sarà più veloce del multiprocessing in generale, soprattutto su Windows, poiché Windows non supporta il fork, quindi ogni nuovo processo deve richiedere del tempo per l'avvio. Su Linux o Mac probabilmente saranno più vicini.

È possibile nidificare più thread all'interno di più processi, ma si consiglia di non utilizzare più thread per escludere più processi.


ThreadPoolExecutor ignora le limitazioni imposte da GIL? inoltre non dovresti iscriverti a () per aspettare che finiscano gli esecutori o questo è implicitamente curato all'interno del gestore del contesto
PirateApp

1
No e no, sì, "gestito implicitamente"
Aaron Hall

Per qualche motivo, quando si ridimensiona il problema, il multithreading è estremamente veloce, ma il multiprocessing genera un sacco di processi bloccati (in macOS). Qualche idea sul perché potrebbe essere? Il processo contiene solo cicli annidati e matematica, niente di esotico.
komodovaran_

@komodovaran_ Un processo è un processo Python completo, uno per ciascuno, mentre un thread è solo un thread di esecuzione con il proprio stack che condivide il processo, il suo bytecode e tutto ciò che ha in memoria con tutti gli altri thread - aiuta ?
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)

Quanto sopra funziona perfettamente sulla mia macchina (Ubuntu, il pacchetto joblib era preinstallato, ma può essere installato tramite pip install joblib ).

Tratto da https://blog.dominodatalab.com/simple-parallelization/


3
Ho provato il tuo codice ma sul mio sistema la versione sequenziale di questo codice impiega circa mezzo minuto e la versione parallela sopra richiede 4 minuti. Perchè così?
Shaifali Gupta,

3
Grazie per la tua risposta! Penso che questo sia il modo più elegante per farlo nel 2019.
Heikki Pulkkinen il

2
il multiprocessing non è valido per Python 3.x, quindi questo non funziona per me.
EngrStudent,

2
@EngrStudent Non sono sicuro del significato di "non valido". Funziona con Python 3.6.x per me.
tyrex,

@tyrex grazie per la condivisione! questo pacchetto joblib è fantastico e l'esempio funziona per me. Tuttavia, in un contesto più complesso, purtroppo ho avuto un bug. github.com/joblib/joblib/issues/949
Open Food Broker

13

I vantaggi dell'utilizzo di Ray sono numerosi :

  • È possibile parallelizzare più macchine oltre a più core (con lo stesso codice).
  • Gestione efficiente dei dati numerici attraverso la memoria condivisa (e serializzazione a zero copie).
  • Elevata produttività delle attività con pianificazione distribuita.
  • Tolleranza ai guasti.

Nel tuo caso, potresti avviare Ray e definire una funzione remota

import ray

ray.init()

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

e quindi invocarlo in parallelo

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)

Per eseguire lo stesso esempio su un cluster, l'unica linea che cambierebbe sarebbe la chiamata a ray.init (). La documentazione pertinente è disponibile qui .

Nota che sto aiutando a sviluppare Ray.


1
Per chiunque stia considerando ray, potrebbe essere importante sapere che non supporta nativamente Windows. Alcuni hack per farlo funzionare in Windows utilizzando WSL (sottosistema Windows per Linux) sono possibili, anche se è quasi impossibile se si desidera utilizzare Windows.
OscarVanL

9

Questo è il modo più semplice per farlo!

Puoi usare asyncio . (La documentazione può essere trovata qui ). Viene utilizzato come base per più framework asincroni Python che forniscono reti e server Web ad alte prestazioni, librerie di connessione al database, code di attività distribuite, ecc. Inoltre ha API sia di alto livello che di basso livello per soddisfare qualsiasi tipo di problema .

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

Ora questa funzione verrà eseguita in parallelo ogni volta che viene chiamata senza mettere il programma principale in stato di attesa. Puoi usarlo per parallelizzare anche per loop. Quando viene chiamato per un ciclo for, sebbene il ciclo sia sequenziale ma ogni iterazione viene eseguita in parallelo al programma principale non appena l'interprete arriva lì. Per esempio:

@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')

Questo produce il seguente output:

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

Penso che ci sia un refuso wrapped()e dovrebbe essere **kwargsinvece di*kwargs
jakub-olczyk il

Oops! Errore mio. Corretto!
Utente 5

6

perché non usi i thread e un mutex per proteggere un elenco globale?

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

tieni presente che sarai veloce quanto il tuo thread più lento


2
So che questa è una risposta molto vecchia, quindi è una seccatura ottenere un downvote casuale dal nulla. Ho solo effettuato il downgrade perché i thread non parallelizzeranno nulla. I thread in Python sono associati a un solo thread in esecuzione sull'interprete alla volta a causa del blocco dell'interprete globale, quindi supportano la programmazione concorrente, ma non sono paralleli come OP richiede.
skrrgwasme,

3
@skrrgwasme So che lo sai, ma quando usi le parole "non metteranno in parallelo nulla", ciò potrebbe indurre in errore i lettori. Se le operazioni impiegano molto tempo perché sono legate all'IO o dormono mentre aspettano un evento, l'interprete viene liberato per eseguire gli altri thread, quindi ciò si tradurrà nell'aumento della velocità che le persone sperano in quei casi. Solo i thread associati alla CPU sono realmente influenzati da ciò che dice skrrgwasme.
Jonathan Hartley,

5

Ho trovato joblibmolto utile con me. Si prega di vedere il seguente esempio:

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: usa tutti i core disponibili


14
Sai, è meglio controllare le risposte già esistenti prima di pubblicare le tue. Questa risposta propone anche di usare joblib.
sanyash,

2

Diciamo che abbiamo una funzione asincrona

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

Deve essere eseguito su un array di grandi dimensioni. Alcuni attributi vengono passati al programma e alcuni vengono utilizzati dalla proprietà dell'elemento del dizionario nell'array.

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

Dai un'occhiata a questo;

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

Questo potrebbe non essere il modo giusto per farlo, ma farei qualcosa del genere;

Codice reale;

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()

Spero che aiuti.


1

Ciò potrebbe essere utile durante l'implementazione di multiprocessing e calcolo parallelo / distribuito in Python.

Tutorial YouTube sull'uso del pacchetto techila

Techila è un middleware di elaborazione distribuito, che si integra direttamente con Python utilizzando il pacchetto techila. La funzione peach nel pacchetto può essere utile per parallelizzare le strutture ad anello. (Il seguente frammento di codice proviene dai forum della community 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
Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia.
SL Barth - Ripristina Monica il

2
@SLBarth grazie per il feedback. Ho aggiunto un piccolo codice di esempio alla risposta.
TEe

1

grazie @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. Questa è una risposta di solo codice. Suggerirei di aggiungere una spiegazione che dice ai lettori cosa fa il codice che hai pubblicato e forse dove possono trovare ulteriori informazioni.
Starbeamrainbowlabs

-1

è un esempio molto semplice di elaborazione parallela

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
Non c'è parallelismo nel ciclo for qui, stai solo generando un processo che esegue l'intero ciclo; questo NON è ciò che intendeva l'OP.
facuq
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.