decidere tra sottoprocesso, multiprocessing e thread in Python?


110

Vorrei parallelizzare il mio programma Python in modo che possa utilizzare più processori sulla macchina su cui viene eseguito. La mia parallelizzazione è molto semplice, in quanto tutti i "thread" paralleli del programma sono indipendenti e scrivono il loro output su file separati. Non ho bisogno dei thread per scambiare informazioni, ma è imperativo sapere quando i thread finiscono poiché alcuni passaggi della mia pipeline dipendono dal loro output.

La portabilità è importante, in quanto mi piacerebbe che funzionasse su qualsiasi versione di Python su Mac, Linux e Windows. Dati questi vincoli, qual è il modulo Python più appropriato per implementarlo? Sto cercando di decidere tra thread, sottoprocesso e multiprocessing, che sembrano tutti fornire funzionalità correlate.

Qualche idea su questo? Vorrei la soluzione più semplice che sia portatile.


Correlati: stackoverflow.com/questions/1743293/… (leggi la mia risposta lì per vedere perché i thread non sono un antipasto per il codice Python puro)

1
"Qualsiasi versione di Python" è di gran lunga troppo vaga. Python 2.3? 1.x? 3.x? È semplicemente una condizione impossibile da soddisfare.
detly

Risposte:


64

multiprocessingè un ottimo modulo tipo coltellino svizzero. È più generale dei thread, poiché puoi persino eseguire calcoli remoti. Questo è quindi il modulo che ti suggerirei di utilizzare.

Il subprocessmodulo consentirebbe anche di avviare più processi, ma l'ho trovato meno comodo da usare rispetto al nuovo modulo multiprocessing.

I thread sono notoriamente sottili e, con CPython, sei spesso limitato a un core, con loro (anche se, come notato in uno dei commenti, il Global Interpreter Lock (GIL) può essere rilasciato in codice C chiamato da codice Python) .

Credo che la maggior parte delle funzioni dei tre moduli che citi possano essere utilizzate in modo indipendente dalla piattaforma. Dal punto di vista della portabilità, nota che multiprocessingviene fornito di serie solo da Python 2.6 (esiste una versione per alcune versioni precedenti di Python, però). Ma è un ottimo modulo!


1
per un incarico, ho semplicemente usato il modulo "multiprocessing" e il suo metodo pool.map (). pezzo di torta !
kmonsoor

Anche una cosa come il sedano è in esame? Perché lo è o non lo è?
user3245268

Per quanto posso dire, Celery è più coinvolto (devi installare un broker di messaggi), ma è un'opzione che probabilmente dovrebbe essere considerata, a seconda del problema in questione.
Eric O Lebigot

186

Per me questo è in realtà piuttosto semplice:

L' opzione sottoprocesso :

subprocessè per eseguire altri eseguibili --- è fondamentalmente un wrapper intorno os.fork()e os.execve()con un po 'di supporto per l'impianto idraulico opzionale (impostazione di PIPE da e verso i sottoprocessi. Ovviamente potresti altri meccanismi di comunicazione tra processi (IPC), come socket, o Posix o SysV ha condiviso la memoria, ma sarai limitato a qualsiasi interfaccia e canale IPC supportati dai programmi che stai chiamando.

Comunemente, uno usa qualsiasi in modo subprocesssincrono --- semplicemente chiamando qualche utility esterna e rileggendo il suo output o aspettando il suo completamento (magari leggendo i suoi risultati da un file temporaneo, o dopo averli inviati a qualche database).

Tuttavia si possono generare centinaia di sottoprocessi e interrogarli. La mia classe di utilità preferita personale fa esattamente questo. Il più grande svantaggio del subprocessmodulo è che il supporto I / O è generalmente bloccante. C'è una bozza di PEP-3145 per risolverlo in qualche futura versione di Python 3.x e un asyncproc alternativo (Attenzione che porta direttamente al download, non a nessun tipo di documentazione né README). Ho anche scoperto che è relativamente facile importare fcntle manipolare Popendirettamente i descrittori di file PIPE, anche se non so se è portabile su piattaforme non UNIX.

(Aggiornamento: 7 agosto 2019: supporto Python 3 per sottoprocessi ayncio : sottoprocessi asyncio )

subprocess non ha quasi alcun supporto per la gestione degli eventi ... anche se puoi usare il signalmodulo e semplici segnali UNIX / Linux della vecchia scuola --- uccidendo i tuoi processi dolcemente, per così dire.

L' opzione multiprocessing :

multiprocessingserve per eseguire funzioni all'interno del codice esistente (Python) con supporto per comunicazioni più flessibili tra questa famiglia di processi. In particolare, è meglio costruire il tuo multiprocessingIPC attorno agli Queueoggetti del modulo, ove possibile, ma puoi anche utilizzare Eventoggetti e varie altre funzionalità (alcune delle quali, presumibilmente, sono costruite attorno al mmapsupporto sulle piattaforme in cui tale supporto è sufficiente).

Il multiprocessingmodulo di Python ha lo scopo di fornire interfacce e funzionalità molto simili a quelle che threading consentono a CPython di ridimensionare l'elaborazione tra più CPU / core nonostante il GIL (Global Interpreter Lock). Sfrutta tutto l'impegno di blocco e coerenza SMP a grana fine svolto dagli sviluppatori del kernel del sistema operativo.

L' opzione di filettatura :

threadingè per una gamma piuttosto ristretta di applicazioni che sono vincolate a I / O (non è necessario scalare su più core della CPU) e che beneficiano della latenza estremamente bassa e dell'overhead di commutazione della commutazione dei thread (con memoria core condivisa) rispetto al processo / cambio di contesto. Su Linux questo è quasi il set vuoto (i tempi di commutazione del processo Linux sono estremamente vicini ai suoi thread-switch).

threadingsoffre di due principali svantaggi in Python .

Uno, ovviamente, è l'implementazione specifica, che interessa principalmente CPython. Questo è il GIL. Per la maggior parte, la maggior parte dei programmi CPython non trarrà vantaggio dalla disponibilità di più di due CPU (core) e spesso le prestazioni risentiranno della contesa di blocco GIL.

Il problema più grande che non è specifico dell'implementazione, è che i thread condividono la stessa memoria, gestori di segnali, descrittori di file e alcune altre risorse del sistema operativo. Quindi il programmatore deve essere estremamente attento al blocco degli oggetti, alla gestione delle eccezioni e ad altri aspetti del loro codice che sono entrambi sottili e che possono uccidere, bloccare o bloccare l'intero processo (suite di thread).

In confronto, il multiprocessingmodello fornisce a ciascun processo la propria memoria, descrittori di file, ecc. Un arresto anomalo o un'eccezione non gestita in uno qualsiasi di essi ucciderà solo quella risorsa e gestire in modo robusto la scomparsa di un processo figlio o fratello può essere considerevolmente più semplice del debugging, dell'isolamento e risolvere o aggirare problemi simili nei thread.

  • (Nota: l'uso di threadingcon i principali sistemi Python, come NumPy , può soffrire notevolmente meno della contesa GIL rispetto alla maggior parte del tuo codice Python. Questo perché sono stati specificamente progettati per farlo; le parti native / binarie di NumPy, ad esempio, rilascerà il GIL quando è sicuro).

L' opzione contorta :

Vale anche la pena notare che Twisted offre un'altra alternativa elegante e allo stesso tempo molto difficile da capire . Fondamentalmente, con il rischio di semplificare eccessivamente al punto in cui i fan di Twisted potrebbero prendere d'assalto la mia casa con forconi e torce, Twisted fornisce il multi-tasking cooperativo guidato dagli eventi all'interno di qualsiasi (singolo) processo.

Per capire come ciò sia possibile, si dovrebbe leggere le caratteristiche di select()(che possono essere costruite attorno a select () o poll () o simili chiamate di sistema OS). Fondamentalmente è tutto guidato dalla possibilità di fare una richiesta al sistema operativo di dormire in attesa di qualsiasi attività su un elenco di descrittori di file o di un timeout.

Il risveglio da ciascuna di queste chiamate a select()è un evento --- uno che coinvolge input disponibile (leggibile) su un certo numero di socket o descrittori di file, o lo spazio di buffer diventa disponibile su altri descrittori o socket (scrivibili), alcune condizioni eccezionali (TCP pacchetti PUSH fuori banda, ad esempio) o un TIMEOUT.

Pertanto, il modello di programmazione Twisted è costruito intorno alla gestione di questi eventi e quindi al looping sul gestore "principale" risultante, permettendogli di inviare gli eventi ai gestori.

Personalmente penso al nome, Twisted come evocativo del modello di programmazione ... poiché il tuo approccio al problema deve essere, in un certo senso, "contorto" al rovescio. Piuttosto che concepire il tuo programma come una serie di operazioni su dati di input e output o risultati, stai scrivendo il tuo programma come servizio o daemon e definisci come reagisce a vari eventi. (In effetti il ​​nucleo "ciclo principale" di un programma Twisted è (di solito? Sempre?) A reactor()).

Le sfide principali nell'utilizzo di Twisted riguardano il distogliere la mente dal modello basato sugli eventi e anche evitare l'uso di librerie di classi o toolkit che non sono scritti per cooperare all'interno del framework Twisted. Questo è il motivo per cui Twisted fornisce i propri moduli per la gestione del protocollo SSH, per curses e le proprie funzioni di sottoprocesso / Popen, e molti altri moduli e gestori di protocollo che, a prima vista, sembrerebbero duplicare le cose nelle librerie standard Python.

Penso che sia utile capire Twisted a livello concettuale anche se non si intende mai utilizzarlo. Può fornire informazioni sulle prestazioni, i conflitti e la gestione degli eventi nel threading, nel multiprocessing e persino nella gestione di sottoprocessi, nonché in qualsiasi elaborazione distribuita intrapresa.

( Nota: le versioni più recenti di Python 3.x includono funzionalità asyncio (asynchronous I / O) come async def , il decoratore @ async.coroutine e la parola chiave await , e sono rese disponibili dal supporto futuro . Tutte queste sono più o meno simili a Twisted from a process (co-operative multitasking) prospettiva). (Per lo stato attuale del supporto Twisted per Python 3, controlla: https://twistedmatrix.com/documents/current/core/howto/python3.html )

L' opzione distribuita :

Ancora un altro ambito dell'elaborazione di cui non hai chiesto, ma che vale la pena considerare, è quello dell'elaborazione distribuita . Esistono molti strumenti e framework Python per l'elaborazione distribuita e il calcolo parallelo. Personalmente penso che il più facile da usare sia quello che viene considerato meno spesso in quello spazio.

È quasi banale creare un'elaborazione distribuita attorno a Redis . L'intero archivio chiavi può essere utilizzato per memorizzare unità di lavoro e risultati, le LISTE Redis possono essere utilizzate Queue()come oggetti simili e il supporto PUB / SUB può essere utilizzato per la Eventgestione simile. È possibile eseguire l'hashing delle chiavi e utilizzare i valori, replicati su un cluster libero di istanze Redis, per archiviare la topologia e le mappature hash-token per fornire hash e failover coerenti per il ridimensionamento oltre la capacità di ogni singola istanza per il coordinamento dei lavoratori e dati di marshalling (pickled, JSON, BSON o YAML) tra di loro.

Ovviamente quando inizi a costruire una soluzione su scala più ampia e più sofisticata attorno a Redis, stai reimplementando molte delle funzionalità che sono già state risolte utilizzando, Celery , Apache Spark e Hadoop , Zookeeper , ecc. , Cassandra e così via. Tutti hanno moduli per l'accesso Python ai loro servizi.

[Aggiornamento: un paio di risorse da prendere in considerazione se stai prendendo in considerazione Python per un uso intensivo di calcolo su sistemi distribuiti: IPython Parallel e PySpark . Sebbene si tratti di sistemi informatici distribuiti di uso generale, sono sottosistemi particolarmente accessibili e diffusi di data science e analytics].

Conclusione

Ecco la gamma di alternative di elaborazione per Python, da thread singolo, con semplici chiamate sincrone a sottoprocessi, pool di sottoprocessi con polling, thread e multiprocessing, multi-tasking cooperativo basato su eventi e elaborazione distribuita.


1
Tuttavia è difficile utilizzare il multiprocessing con le classi / OOP.
Tjorriemorrie

2
@Tjorriemorrie: Immagino che tu intenda che è difficile inviare chiamate di metodo a istanze di oggetti che potrebbero essere in altri processi. Suggerirei che questo è lo stesso problema che avresti con i thread, ma più facilmente visibile (piuttosto che essere fragile e soggetto a condizioni di gara oscure). Penso che l'approccio consigliato sia quello di fare in modo che tutto questo invio avvenga tramite oggetti Queue, che funzionano a thread singolo, multi-thread e tra processi. (Con alcune implementazioni Redis o Celery Queue, anche su un cluster di nodi)
Jim Dennis

2
Questa è davvero una buona risposta. Vorrei che fosse nell'introduzione alla concorrenza nei documenti di Python3.
root-11

1
@ root-11 puoi proporlo ai manutentori del documento; L'ho pubblicato qui per uso gratuito. Tu e loro potete usarlo, intero o in parte.
Jim Dennis

"Per me questo è in realtà piuttosto semplice:" Lo adoro. grazie mille
girolamo

5

In un caso simile ho optato per processi separati e un po 'di comunicazione necessaria tramite presa di rete. È altamente portabile e abbastanza semplice da fare usando python, ma probabilmente non il più semplice (nel mio caso avevo anche un altro vincolo: la comunicazione con altri processi scritti in C ++).

Nel tuo caso probabilmente sceglierei il multiprocesso, poiché i thread python, almeno quando si usa CPython, non sono thread reali. Bene, sono thread di sistema nativi ma i moduli C chiamati da Python possono o meno rilasciare il GIL e consentire ad altri thread di essere eseguiti quando si chiama codice di blocco.


4

Per utilizzare più processori in CPython la tua unica scelta è il multiprocessingmodulo. CPython mantiene un blocco sui suoi interni (il GIL ) che impedisce ai thread su altre CPU di funzionare in parallelo. Il multiprocessingmodulo crea nuovi processi (come subprocess) e gestisce la comunicazione tra di loro.


5
Non è del tutto vero, AFAIK puoi rilasciare il GIL usando l'API C, e ci sono altre implementazioni di Python come IronPython o Jython che non soffrono di tali limitazioni. Non ho votato in negativo però.
Bastien Léonard

1

Sborsare e lasciare che l'unix faccia il tuo lavoro:

usa iterpipes per avvolgere il sottoprocesso e poi:

Dal sito di Ted Ziuba

INPUTS_FROM_YOU | xargs -n1 -0 -P NUM ./process #NUM processi paralleli

O

Servirà anche Gnu Parallel

Esci con GIL mentre mandi i ragazzi dietro le quinte a fare il tuo lavoro multicore.


6
"La portabilità è importante, in quanto mi piacerebbe che funzionasse su qualsiasi versione di Python su Mac, Linux e Windows."
detly

Con questa soluzione puoi interagire ripetutamente con il lavoro? Puoi farlo in multiprocessing, ma non credo sia così in subprocess.
abalter
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.