Il modulo `concurrent` in Python

Il modulo `concurrent` in Python

In questo articolo spiegheremo il modulo concurrent in Python.

Chiarendo i concetti di concorrenza e parallelismo, spiegheremo come implementare l'elaborazione asincrona usando il modulo concurrent con esempi pratici.

YouTube Video

Il modulo concurrent in Python

Quando si vuole velocizzare l'elaborazione in Python, è importante tenere a mente le differenze tra concorrenza e parallelismo. Il modulo concurrent è uno strumento importante per gestire l'elaborazione asincrona in modo sicuro e semplice tenendo conto di queste differenze.

La differenza tra concorrenza e parallelismo

  • La concorrenza consiste nel progettare un processo in modo che più compiti avanzino alternandosi tra di loro in unità di lavoro ridotte. Anche se i compiti non vengono eseguiti effettivamente allo stesso tempo, sfruttare il "tempo di attesa" permette di rendere più efficiente il processo complessivo.

  • Il parallelismo è un meccanismo che esegue fisicamente più compiti contemporaneamente. Utilizzando più core della CPU, l'elaborazione avviene simultaneamente.

Entrambe sono tecniche per velocizzare l'elaborazione, ma la concorrenza riguarda il progetto di "come procedere", mentre il parallelismo riguarda l'esecuzione di "come viene effettivamente eseguito", rendendole fondamentalmente diverse.

Cos'è il modulo concurrent?

concurrent è una libreria standard di Python che offre una API di alto livello per gestire concorrenza e parallelismo in modo sicuro e semplice. È progettato in modo che tu possa concentrarti sull'‘esecuzione dei compiti’ senza preoccuparti delle operazioni di basso livello come la creazione e la gestione di thread o processi.

Ruoli di ThreadPoolExecutor e ProcessPoolExecutor

Il modulo concurrent offre due opzioni principali a seconda della natura del compito.

  • ThreadPoolExecutor Questa soluzione è adatta alle implementazioni concorrenti, specialmente per compiti con molte attese I/O, come operazioni di rete o su file. Alternando fra i compiti, utilizza in modo efficiente il tempo di attesa.

  • ProcessPoolExecutor Questa implementazione è orientata all'elaborazione parallela ed è adatta per compiti che richiedono molte risorse CPU. Utilizza più processi per sfruttare al massimo i core della CPU disponibili in parallelo.

Quindi, una caratteristica principale del modulo concurrent è che offre una struttura che ti permette di scegliere correttamente tra concorrenza e parallelismo a seconda delle necessità.

Nozioni di base su ThreadPoolExecutor (per attività di I/O)

ThreadPoolExecutor è adatto per compiti I/O-bound, come comunicazione di rete ed operazioni su file. Distribuisce i compiti tra più thread, sfruttando efficacemente il tempo di attesa.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in futures:
13        print(future.result())
  • In questo esempio, più compiti di I/O che attendono per un secondo sono eseguiti contemporaneamente. Utilizzando submit, le chiamate alle funzioni sono registrate come compiti asincroni, e chiamando result() puoi attendere il completamento e ottenere i risultati, permettendo di implementare l'elaborazione concorrente sfruttando efficacemente il tempo di attesa in modo conciso.

Concorrenza semplice usando map

Se non è necessario un controllo complesso, l'uso di map può rendere il tuo codice più conciso.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    results = executor.map(fetch_data, range(5))
11
12    for result in results:
13        print(result)
  • In questo esempio, più compiti di I/O sono eseguiti in modo concorrente usando ThreadPoolExecutor.map. Poiché map restituisce i risultati nello stesso ordine degli input, puoi scrivere un codice simile a quello sequenziale e ottenere un'esecuzione concorrente senza dover pensare all'elaborazione asincrona—questo è un grande vantaggio.

Nozioni di base su ProcessPoolExecutor (per attività CPU-bound)

Per calcoli pesanti che sfruttano appieno la CPU, dovresti usare processi anziché thread. Questo ti permette di evitare la limitazione del Global Interpreter Lock (GIL).

 1from concurrent.futures import ProcessPoolExecutor
 2
 3def heavy_calculation(n):
 4    # Simulate a CPU-bound task
 5    total = 0
 6    for i in range(10_000_000):
 7        total += i * n
 8    return total
 9
10if __name__ == "__main__":
11    with ProcessPoolExecutor(max_workers=4) as executor:
12        results = executor.map(heavy_calculation, range(4))
13
14        for result in results:
15            print(result)

In questo esempio, calcoli intensivi della CPU sono eseguiti in parallelo usando ProcessPoolExecutor. Poiché viene coinvolta la creazione di processi, è necessario usare il blocco __main__, che permette l'elaborazione parallela sicura usando più core della CPU.

Elaborazione secondo l'ordine di completamento usando as_completed

as_completed è utile quando vuoi gestire i risultati nell'ordine in cui vengono completati.

 1from concurrent.futures import ThreadPoolExecutor, as_completed
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in as_completed(futures):
13        print(future.result())
  • In questo esempio, più compiti asincroni vengono eseguiti simultaneamente ed i risultati sono recuperati nell'ordine in cui vengono completati. Usando as_completed puoi gestire rapidamente i risultati indipendentemente dall'ordine dei compiti, rendendolo adatto per mostrare lo stato di avanzamento o situazioni che richiedono una gestione sequenziale.

Gestione delle eccezioni

In concurrent, le eccezioni vengono sollevate quando chiami result().

 1from concurrent.futures import ThreadPoolExecutor
 2
 3def risky_task(n):
 4    # Simulate a task that may fail for a specific input
 5    if n == 3:
 6        raise ValueError("Something went wrong")
 7    return n * 2
 8
 9with ThreadPoolExecutor() as executor:
10    futures = [executor.submit(risky_task, i) for i in range(5)]
11
12    for future in futures:
13        try:
14            print(future.result())
15        except Exception as e:
16            print("Error:", e)
  • Questo esempio dimostra che anche se alcuni compiti sollevano eccezioni, gli altri continuano l'esecuzione e le eccezioni possono essere gestite individualmente durante il recupero dei risultati. Utilizzando i Future di concurrent, è fondamentale poter gestire in sicurezza sia i successi sia i fallimenti dell’elaborazione asincrona.

Linee guida per la scelta tra thread e processi

Per utilizzare efficacemente concorrenza e parallelismo, è importante scegliere l'approccio giusto in base alla natura del compito.

Nella pratica, i seguenti criteri possono aiutarti a decidere.

  • Per i processi con molte attese di I/O, come la comunicazione o le operazioni sui file, utilizzare ThreadPoolExecutor.
  • Per compiti CPU-bound, con calcoli pesanti, utilizza ProcessPoolExecutor.
  • Se ci sono molti compiti semplici, usare map permette di scrivere codice più conciso.
  • Se è importante avere un controllo preciso sull'ordine di esecuzione o sulla gestione delle eccezioni, combina submit con as_completed.

Vantaggi dell'uso di concurrent

Utilizzando il modulo concurrent, puoi gestire l'elaborazione asincrona in modo sicuro e intuitivo.

I principali vantaggi sono i seguenti:.

  • Non devi preoccuparti della gestione a basso livello di thread o processi.
  • Fa parte della libreria standard di Python, quindi puoi usarlo con tranquillità.
  • Il codice diventa più leggibile e manutenibile.
  • È ideale come primo passo per imparare la concorrenza e il parallelismo.

Tenendo semplicemente presenti queste linee guida, si possono ridurre notevolmente gli errori nelle implementazioni che utilizzano concurrent.

Riepilogo

Il modulo concurrent è l'opzione standard per la concorrenza e il parallelismo pratico in Python. Permette di migliorare le prestazioni senza modificare molto il contenuto dell'elaborazione, il che è un vantaggio significativo nella pratica reale. Utilizzando concurrent, puoi implementare l'elaborazione asincrona in modo conciso gestendo in sicurezza le eccezioni e il controllo dell'esecuzione.

Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.

YouTube Video