Quali sono le differenze tra i moduli di threading e multiprocessing?


141

Sto imparando come utilizzare il threadinged i multiprocessingmoduli in Python per eseguire alcune operazioni in parallelo e velocizzare il mio codice.

Lo trovo difficile (forse perché non ho alcun background teorico al riguardo) per capire quale sia la differenza tra un threading.Thread()oggetto e multiprocessing.Process()uno.

Inoltre, non mi è del tutto chiaro come creare un'istanza di una coda di lavori e averne solo 4 (ad esempio) in esecuzione in parallelo, mentre l'altro aspetta che le risorse vengano liberate prima di essere eseguite.

Trovo gli esempi nella documentazione chiari, ma non molto esaustivi; non appena provo a complicare un po 'le cose, ricevo molti strani errori (come un metodo che non può essere decapato, e così via).

Quindi, quando dovrei usare i moduli threadinge multiprocessing?

Potete collegarmi ad alcune risorse che spiegano i concetti alla base di questi due moduli e come usarli correttamente per compiti complessi?


C'è di più, c'è anche il Threadmodulo (chiamato _threadin Python 3.x). Ad essere sincero, non ho mai capito le differenze da solo ...
Non so

3
@Dunno: come dice esplicitamente la documentazione Thread/ _thread, si tratta di "primitive di basso livello". Potresti usarlo per costruire oggetti di sincronizzazione personalizzati, per controllare l'ordine di join di un albero di thread, ecc. Se non riesci a immaginare perché dovresti usarlo, non usarlo e attenersi threading.
abarnert,

Risposte:


260

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.futureslibreria 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 Futureoggetti 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.Threado multiprocessing.Processinvece di fare affidamento sui pool. E dovrai creare queue.Queueo multiprocessing.Queueoggetti 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:

  1. Le discussioni condividono i dati per impostazione predefinita; i processi no.
  2. Come conseguenza di (1), l'invio di dati tra processi generalmente richiede il decapaggio e il disimballaggio. **
  3. 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 ctypestipi.
  4. I processi non sono soggetti al GIL.
  5. Su alcune piattaforme (principalmente Windows), i processi sono molto più costosi da creare e distruggere.
  6. Esistono alcune restrizioni aggiuntive sui processi, alcune delle quali sono diverse su piattaforme diverse. Vedere le linee guida di programmazione per i dettagli.
  7. Il threadingmodulo non ha alcune delle funzionalità del multiprocessingmodulo. (È possibile utilizzare multiprocessing.dummyper ottenere la maggior parte dell'API mancante in cima ai thread oppure è possibile utilizzare moduli di livello superiore come concurrent.futurese 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.


grazie, ma non sono sicuro di aver capito tutto. Comunque sto provando a farlo un po 'per scopi di apprendimento, e un po' perché con un uso ingenuo del thread ho dimezzato la velocità del mio codice (avviando più di 1000 thread contemporaneamente, chiamando ciascuno un'app esterna ... la cpu, ma c'è un aumento di x2 della velocità). Penso che gestire il thread in modo intelligente potrebbe davvero migliorare la velocità del mio codice ..
lucacerone,

3
@LucaCerone: Ah, se il tuo codice trascorre la maggior parte del suo tempo in attesa di programmi esterni, allora sì, trarrà beneficio dal threading. Buon punto. Vorrei modificare la risposta per spiegarlo.
abarnert,

2
@LucaCerone: Nel frattempo, quali parti non capisci? Senza conoscere il livello di conoscenza con cui stai iniziando, è difficile scrivere una buona risposta ... ma con un feedback, forse possiamo trovare qualcosa di utile per te e anche per i futuri lettori.
abarnert,

3
@LucaCerone Dovresti leggere il PEP per il multiprocessing qui . Fornisce tempistiche ed esempi di thread vs multiprocessing.
mr2ert,

1
@LucaCerone: se l'oggetto a cui è associato il metodo non ha alcuno stato complesso, la soluzione più semplice per il problema del decapaggio è scrivere una stupida funzione wrapper che genera l'oggetto e chiama il suo metodo. Se non hanno stato complesso, allora probabilmente necessario rendere serializzabili (che è abbastanza facile, i pickledocumenti spiegano), e quindi nel peggiore dei casi il tuo involucro stupido è def wrapper(obj, *args): return obj.wrapper(*args).
abarnert,

32

Più thread possono esistere in un singolo processo. I thread che appartengono allo stesso processo condividono la stessa area di memoria (possono leggere e scrivere sulle stesse variabili e possono interferire tra loro). Al contrario, processi diversi vivono in aree di memoria diverse e ognuna di esse ha le sue variabili. Per comunicare, i processi devono utilizzare altri canali (file, pipe o socket).

Se vuoi parallelizzare un calcolo, probabilmente avrai bisogno del multithreading, perché probabilmente vuoi che i thread cooperino sulla stessa memoria.

Parlando di prestazioni, i thread sono più veloci da creare e gestire rispetto ai processi (perché il sistema operativo non ha bisogno di allocare una nuova area di memoria virtuale) e la comunicazione tra thread è generalmente più veloce della comunicazione tra processi. Ma i thread sono più difficili da programmare. I thread possono interferire l'uno con l'altro e possono scrivere nella memoria reciproca, ma il modo in cui ciò accade non è sempre ovvio (a causa di diversi fattori, principalmente il riordino delle istruzioni e la memorizzazione nella cache della memoria), quindi avrai bisogno di primitive di sincronizzazione per controllare l'accesso alle tue variabili.


12
Questo manca alcune informazioni molto importanti sul GIL, che lo rende fuorviante.
abarnert,

1
@ mr2ert: Sì, questa è l'informazione molto importante in breve. :) Ma è un po 'più complicato di quello, motivo per cui ho scritto una risposta separata.
abarnert,

2
Pensavo di aver commentato dicendo che @abarnert ha ragione, e mi sono dimenticato del GIL nel rispondere qui. Quindi questa risposta è sbagliata, non dovresti votarla.
Giulio Franco,

6
Ho votato in negativo questa risposta perché non risponde ancora qual è la differenza tra Python threadinge multiprocessing.
Antti Haapala,

Ho letto che esiste un GIL per ogni processo. Ma tutti i processi usano lo stesso interprete python o esiste un interprete separato per thread?
variabile

3

Credo che questo link risponda alla tua domanda in modo elegante.

Per essere brevi, se uno dei tuoi sotto-problemi deve attendere mentre un altro termina, il multithreading è buono (nelle operazioni di I / O pesanti, ad esempio); al contrario, se i tuoi sotto-problemi potrebbero realmente verificarsi contemporaneamente, si consiglia il multiprocessing. Tuttavia, non creerai più processi del tuo numero di core.


3

Citazioni di documentazione Python

Ho evidenziato le principali citazioni della documentazione di Python su Process vs Threads e GIL in: Qual è il blocco dell'interprete globale (GIL) in CPython?

Esperimenti di processo vs thread

Ho fatto un po 'di benchmarking per mostrare la differenza in modo più concreto.

Nel benchmark, ho cronometrato il lavoro con CPU e IO per vari numeri di thread su un hyperthread 8 CPU . Il lavoro fornito per thread è sempre lo stesso, quindi più thread significa più lavoro totale fornito.

I risultati furono:

inserisci qui la descrizione dell'immagine

Traccia dati .

conclusioni:

  • per il lavoro associato alla CPU, il multiprocessing è sempre più veloce, presumibilmente a causa del GIL

  • per il lavoro con IO associato. entrambi hanno esattamente la stessa velocità

  • i thread si ridimensionano solo di circa 4x invece dell'8 previsto poiché sono su una macchina 8 hyperthread.

    Contrastalo con un lavoro associato alla CPU POSIX C che raggiunge la velocità prevista 8 volte superiore: cosa significano "reale", "utente" e "sys" nell'output del tempo (1)?

    TODO: Non ne conosco il motivo, devono esserci altre inefficienze di Python che entrano in gioco.

Codice di prova:

#!/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 upstream + traccia il codice sulla stessa directory .

Testato su Ubuntu 18.10, Python 3.6.7, in un laptop Lenovo ThinkPad P51 con CPU: CPU Intel Core i7-7820HQ (4 core / 8 thread), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).

Visualizza quali thread sono in esecuzione in un determinato momento

Questo post https://rohanvarma.me/GIL/ mi ha insegnato che puoi eseguire un callback ogni volta che un thread è programmato con l' target=argomento dithreading.Thread e lo stesso per multiprocessing.Process.

Questo ci consente di visualizzare esattamente quale thread viene eseguito ogni volta. Al termine, vedremmo qualcosa di simile (ho creato questo particolare grafico):

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

che mostrerebbe che:

  • i thread sono completamente serializzati da GIL
  • i processi possono essere eseguiti in parallelo

1

Ecco alcuni dati sulle prestazioni di python 2.6.x che chiamano in discussione l'idea che il threading sia più performante del multiprocessing negli scenari con IO associato. Questi risultati provengono da un IBM System x3650 M4 BD a 40 processori.

Elaborazione basata su IO: il pool di processi ha funzionato meglio del pool di thread

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

Elaborazione limitata dalla CPU: il pool di processi ha funzionato meglio del pool di thread

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

Questi non sono test rigorosi, ma mi dicono che il multiprocessing non è del tutto non performante rispetto al threading.

Codice utilizzato nella console interattiva di Python per i test precedenti

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

Ho usato il tuo codice (rimosso la parte globale ) e ho trovato questi risultati interessanti con 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

Bene, la maggior parte della domanda risponde a Giulio Franco. Spiegherò ulteriormente il problema del consumatore-produttore, che suppongo vi porterà sulla strada giusta per la vostra soluzione all'utilizzo di un'app multithread.

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)

Puoi leggere di più sulle primitive di sincronizzazione da:

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

Lo pseudocodice è sopra. Suppongo che dovresti cercare il problema produttore-consumatore per ottenere più riferimenti.


scusa innosam, ma questo mi sembra C ++? grazie per i collegamenti :)
lucacerone,

In realtà, le idee alla base del multiprocessing e del multithreading sono indipendenti dalla lingua. La soluzione sarebbe simile al codice sopra.
Innosam,

2
Questo non è C ++; è uno pseudocodice (o è un codice per un linguaggio prevalentemente tipicamente dinamico con una sintassi di tipo C. Detto questo, penso che sia più utile scrivere uno pseudocodice simile a Python per insegnare agli utenti Python. (Soprattutto dal momento che lo psuedocode simile a Python spesso risulta essere un codice eseguibile, o almeno vicino ad esso, il che è raramente vero per lo pseudocodice di tipo C ...)
abarnert

L'ho riscritto come pseudocodice simile a Python (usando anche OO e passando parametri invece di usare oggetti globali); sentiti libero di tornare indietro se pensi che renda le cose meno chiare.
abarnert,

Inoltre, vale la pena notare che lo stdlib di Python ha una coda sincronizzata integrata che racchiude tutti questi dettagli e le sue API del pool di processi e thread astraggono ulteriormente le cose. Vale sicuramente la pena di capire come funzionano le code sincronizzate sotto le copertine, ma raramente dovrai scriverne una tu.
abarnert,
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.