Ciò che dice Giulio Franco è vero per il multithreading e il multiprocessing in generale .
Tuttavia, Python * ha un ulteriore problema: esiste un blocco dell'interprete globale che impedisce a due thread nello stesso processo di eseguire codice Python contemporaneamente. Ciò significa che se si dispone di 8 core e si modifica il codice per utilizzare 8 thread, non sarà in grado di utilizzare l'800% di CPU ed eseguire 8 volte più veloce; utilizzerà la stessa CPU al 100% e funzionerà alla stessa velocità. (In realtà, funzionerà un po 'più lentamente, perché c'è un sovraccarico extra dal threading, anche se non hai dati condivisi, ma per il momento ignoralo.)
Ci sono delle eccezioni. Se il calcolo pesante del tuo codice non si verifica effettivamente in Python, ma in alcune librerie con codice C personalizzato che esegue la corretta gestione GIL, come un'app numpy, otterrai il vantaggio prestazionale previsto dal threading. Lo stesso vale se il calcolo pesante viene eseguito da un sottoprocesso che si esegue e si attende.
Ancora più importante, ci sono casi in cui questo non ha importanza. Ad esempio, un server di rete trascorre la maggior parte del tempo a leggere i pacchetti dalla rete e un'app GUI trascorre la maggior parte del tempo in attesa di eventi dell'utente. Un motivo per utilizzare i thread in un server di rete o un'app GUI è consentire di eseguire "attività in background" di lunga durata senza interrompere il thread principale dal continuare a servire i pacchetti di rete o gli eventi della GUI. E funziona perfettamente con i thread Python. (In termini tecnici, questo significa che i thread di Python ti danno la concorrenza, anche se non ti danno il core parallelismo.)
Ma se stai scrivendo un programma associato alla CPU in puro Python, l'utilizzo di più thread non è generalmente utile.
L'uso di processi separati non ha tali problemi con GIL, poiché ogni processo ha il proprio GIL separato. Ovviamente hai ancora tutti gli stessi compromessi tra thread e processi come in qualsiasi altra lingua: è più difficile e più costoso condividere i dati tra processi che tra thread, può essere costoso eseguire un numero enorme di processi o creare e distruggere frequentemente, ecc. Ma il GIL pesa pesantemente sulla bilancia verso i processi, in un modo che non è vero, diciamo, C o Java. Quindi, ti ritroverai a utilizzare il multiprocessing molto più spesso in Python rispetto a quanto faresti in C o Java.
Nel frattempo, la filosofia "batterie incluse" di Python porta alcune buone notizie: è molto facile scrivere codice che può essere cambiato avanti e indietro tra thread e processi con un cambio di una riga.
Se si progetta il proprio codice in termini di "lavori" autonomi che non condividono nulla con altri lavori (o con il programma principale) ad eccezione di input e output, è possibile utilizzare la concurrent.futures
libreria per scrivere il proprio codice in un pool di thread come questo:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
È anche possibile ottenere i risultati di tali lavori e trasmetterli a ulteriori lavori, attendere le cose in ordine di esecuzione o in ordine di completamento, ecc .; leggi la sezione sugli Future
oggetti per i dettagli.
Ora, se si scopre che il tuo programma utilizza costantemente il 100% di CPU e l'aggiunta di più thread lo rende solo più lento, allora stai riscontrando il problema GIL, quindi devi passare ai processi. Tutto quello che devi fare è cambiare quella prima riga:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
L'unica vera avvertenza è che gli argomenti dei tuoi lavori e i valori di ritorno devono essere selezionabili (e non impiegare troppo tempo o memoria per decapitare) per essere utilizzabili in processi incrociati. Di solito questo non è un problema, ma a volte lo è.
Ma cosa succede se i tuoi lavori non possono essere autonomi? Se riesci a progettare il tuo codice in termini di lavori che passano messaggi da uno all'altro, è ancora abbastanza facile. Potrebbe essere necessario utilizzare threading.Thread
o multiprocessing.Process
invece di fare affidamento sui pool. E dovrai creare queue.Queue
o multiprocessing.Queue
oggetti esplicitamente. (Esistono molte altre opzioni: pipe, socket, file con flock, ... ma il punto è che devi fare qualcosa manualmente se la magia automatica di un Executor è insufficiente.)
Ma cosa succede se non puoi nemmeno fare affidamento sul passaggio dei messaggi? Cosa succede se sono necessari due lavori per mutare entrambi la stessa struttura e vedere i cambiamenti degli altri? In tal caso, sarà necessario eseguire la sincronizzazione manuale (blocchi, semafori, condizioni, ecc.) E, se si desidera utilizzare i processi, espliciti oggetti a memoria condivisa per l'avvio. Questo è quando il multithreading (o multiprocessing) diventa difficile. Se puoi evitarlo, fantastico; se non ci riesci, dovrai leggere più di quanto qualcuno possa inserire in una risposta SO.
Da un commento, volevi sapere cosa c'è di diverso tra thread e processi in Python. Davvero, se leggi la risposta di Giulio Franco e la mia e tutti i nostri link, questo dovrebbe riguardare tutto ... ma una sintesi sarebbe sicuramente utile, quindi ecco qui:
- Le discussioni condividono i dati per impostazione predefinita; i processi no.
- Come conseguenza di (1), l'invio di dati tra processi generalmente richiede il decapaggio e il disimballaggio. **
- Come altra conseguenza di (1), la condivisione diretta dei dati tra i processi richiede generalmente di metterli in formati di basso livello come Valore, Matrice e
ctypes
tipi.
- I processi non sono soggetti al GIL.
- Su alcune piattaforme (principalmente Windows), i processi sono molto più costosi da creare e distruggere.
- Esistono alcune restrizioni aggiuntive sui processi, alcune delle quali sono diverse su piattaforme diverse. Vedere le linee guida di programmazione per i dettagli.
- Il
threading
modulo non ha alcune delle funzionalità del multiprocessing
modulo. (È possibile utilizzare multiprocessing.dummy
per ottenere la maggior parte dell'API mancante in cima ai thread oppure è possibile utilizzare moduli di livello superiore come concurrent.futures
e non preoccuparsene.)
* In realtà non è Python, il linguaggio, ad avere questo problema, ma CPython, l'implementazione "standard" di quel linguaggio. Alcune altre implementazioni non hanno un GIL, come Jython.
** Se si utilizza il metodo fork start per il multiprocessing, che è possibile sulla maggior parte delle piattaforme non Windows, ogni processo figlio ottiene tutte le risorse che il padre aveva all'avvio del figlio, che può essere un altro modo per passare i dati ai bambini.
Thread
modulo (chiamato_thread
in Python 3.x). Ad essere sincero, non ho mai capito le differenze da solo ...