Applicazioni Python | Sfruttare il multiprocessing per velocità ed efficienza

Python Applications | Leveraging multiprocessing for speed and efficiency

Introduzione

Utilizzando le capacità complete dei moderni processori multi-core, il multiprocessing è un concetto fondamentale nell’informatica che consente ai programmi di eseguire numerose attività o processi contemporaneamente. Separando le attività in diversi processi, ognuno con il proprio spazio di memoria, il multiprocessing consente al software di superare i vincoli di prestazioni, a differenza delle tecniche convenzionali a singolo thread. Poiché i processi sono isolati, vi è stabilità e sicurezza perché si evitano conflitti di memoria. In particolare per i lavori legati alla CPU che richiedono operazioni computazionali estese, la capacità del multiprocessing di ottimizzare l’esecuzione del codice è cruciale. È un elemento rivoluzionario per le applicazioni Python in cui velocità ed efficienza sono cruciali, come l’elaborazione dati, le simulazioni scientifiche, l’elaborazione di immagini e video e il machine learning.

Obiettivi di apprendimento

  • Acquisire una solida comprensione del multiprocessing e della sua importanza nell’utilizzo dei moderni processori multi-core per migliorare le prestazioni delle applicazioni Python.
  • Imparare come creare, gestire e sincronizzare più processi utilizzando il modulo ‘multiprocessing’ di Python, consentendo l’esecuzione parallela di attività garantendo al contempo stabilità e integrità dei dati.
  • Scoprire strategie per ottimizzare le prestazioni del multiprocessing, inclusi fattori come la natura delle attività, l’utilizzo delle risorse e la gestione dell’overhead di comunicazione, al fine di sviluppare applicazioni Python efficienti e reattive.
  • Multiprocessing

Sfruttando le capacità dei moderni processori multi-core, il multiprocessing è un approccio potente nella programmazione informatica che consente ai programmi di eseguire numerose attività o processi contemporaneamente. Il multiprocessing genera diversi processi, ognuno con il proprio spazio di memoria, invece del multi-threading, che coinvolge l’esecuzione di più thread all’interno di un singolo processo. Questo isolamento impedisce ai processi di interferire con la memoria l’uno dell’altro, il che migliora la stabilità e la sicurezza.

Questo articolo è stato pubblicato come parte del Data Science Blogathon.

Importanza del multiprocessing nell’ottimizzazione dell’esecuzione del codice

Un obiettivo importante nello sviluppo software è ottimizzare l’esecuzione del codice. La capacità di elaborazione di un singolo core può essere un limite per la programmazione sequenziale tradizionale. Consentendo l’allocazione delle attività su più core, il multiprocessing supera questa limitazione e sfrutta al massimo le capacità dei processori contemporanei. Di conseguenza, i lavori che richiedono molte elaborazioni vengono eseguiti più velocemente e con prestazioni significativamente migliori.

Scenari in cui il multiprocessing è vantaggioso

  • Attività legate alla CPU: Il multiprocessing può portare a miglioramenti significativi delle prestazioni per le applicazioni che richiedono principalmente operazioni computazionali intensive, come calcoli matematici sofisticati o simulazioni complesse. Ogni processo può eseguire una parte del calcolo contemporaneamente per massimizzare l’utilizzo della CPU.
  • Elaborazione parallela: Il multiprocessing consente la gestione simultanea di diverse sottoattività separate, suddividendo molti problemi reali in parti più gestibili. Ciò riduce il tempo totale necessario per completare il compito.
  • Elaborazione di immagini e video: Applicare filtri, modifiche e analisi a diverse porzioni dei media è un aspetto comune nella manipolazione di foto e filmati. Distribuire queste operazioni tra i processi mediante il multiprocessing migliora l’efficienza.
  • Simulazioni scientifiche: Il multiprocessing è vantaggioso per simulazioni complesse come la piegatura delle proteine o la modellazione del tempo. La simulazione può essere eseguita in processi indipendenti, ottenendo risultati più rapidi.
  • Web scraping e crawling: Il multiprocessing può aiutare l’estrazione di informazioni da numerosi siti web ottenendo contemporaneamente dati da diverse fonti, riducendo il tempo necessario per raccogliere informazioni.
  • Server concorrenti: Il multiprocessing è utile nella creazione di server concorrenti, in cui ogni processo gestisce una richiesta client diversa. Ciò impedisce che le richieste più lente ostacolino quelle più veloci.
  • Batch Processing: Accelerare il multiprocessing per il completamento di ogni batch in situazioni in cui le attività devono essere completate a batch.

Comprensione di processi e thread

Il raggiungimento di concorrenza e parallelismo dipende in gran parte dall’uso di processi e thread, le unità di base di esecuzione in un programma informatico.

Processi:

Un’istanza isolata di un programma in uso è un processo. Ogni processo ha il proprio ambiente di esecuzione, spazio di memoria e risorse. Poiché i processi sono separati, non condividono direttamente la memoria. La comunicazione tra processi (IPC) rappresenta uno dei meccanismi più complessi per facilitare la comunicazione tra i processi. Date le loro dimensioni e la separazione intrinseca, i processi eccellono nel gestire attività pesanti, come l’esecuzione di numerosi programmi indipendenti.

Threads:

I thread sono le unità di esecuzione più piccole all’interno di un processo. Possono esistere più thread con le stesse risorse e memoria all’interno di un singolo processo. Poiché condividono lo stesso ambiente di memoria, i thread in esecuzione nello stesso processo possono comunicare tramite variabili condivise. Rispetto ai processi, i thread sono più leggeri e più adatti per attività che coinvolgono grandi quantità di dati condivisi e una leggera separazione.

Limitazioni del Global Interpreter Lock (GIL) e il suo impatto sul Multithreading

Una mutex chiamata Global Interpreter Lock (GIL) viene utilizzata in CPython, l’implementazione di Python più popolare, per sincronizzare l’accesso agli oggetti Python e impedire a più thread di eseguire bytecode Python contemporaneamente nello stesso processo. Questo significa che anche su sistemi con più core, solo un thread può eseguire codice Python contemporaneamente all’interno di un determinato processo.

Implicazioni del GIL

Attività legate all’I/O: Le operazioni legate all’I/O, in cui i thread attendono frequentemente risorse esterne come l’I/O su file o le risposte di rete, sono meno significativamente influenzate dal GIL. Le azioni di blocco e rilascio del GIL hanno un effetto relativamente minore sulle prestazioni in tali circostanze.

Quando utilizzare i Thread e i Processi in Python?

Thread: Quando si gestiscono attività legate all’I/O, i thread sono vantaggiosi quando il software deve attendere a lungo risorse esterne. Possono operare in background senza interferire con il thread principale, rendendoli adatti per applicazioni che richiedono interfacce utente reattive.

Processi: Per operazioni legate alla CPU o quando si desidera utilizzare pienamente più core CPU, i processi sono più appropriati. La multiprogrammazione consente l’esecuzione parallela su più core senza le restrizioni del GIL perché ogni processo ha il proprio GIL.

Il modulo ‘Multiprocessing’

Il modulo di multiprocessing di Python è uno strumento potente per ottenere la concorrenza e la parallelizzazione creando e gestendo diversi processi. Offre un’interfaccia di alto livello per avviare e gestire processi, consentendo ai programmatori di eseguire attività parallele su macchine multicore.

Abilitare l’esecuzione concorrente tramite processi multipli:

Creando numerosi processi distinti, ognuno con il proprio interprete Python e spazio di memoria, il modulo di multiprocessing consente di eseguire contemporaneamente più programmi. Di conseguenza, l’esecuzione parallela reale su piattaforme multicore diventa possibile superando le restrizioni del Global Interpreter Lock (GIL) del modulo di threading predefinito.

Panoramica delle classi e delle funzioni principali

Classe Process:

La classe Process funge da “cervello” del modulo di multiprocessing. È possibile costruire e gestire un processo indipendente utilizzando questa classe, che ne rappresenta uno. Tecniche e attributi essenziali includono:

Start(): Avvia il processo, facendo eseguire la funzione target in un nuovo processo.

Terminate(): Termina il processo in modo forzato.

Classe Queue: La classe Queue offre un metodo sicuro di comunicazione tra processi tramite una coda sincronizzata. Supporta l’aggiunta e la rimozione di elementi dalla coda utilizzando metodi come put() e get().

Classe Pool: È possibile parallelizzare l’esecuzione di una funzione su diversi valori di input grazie alla classe Pool, che controlla un pool di processi worker. Tecniche fondamentali includono:

Pool(processes): Costruttore per creare un pool di processi con un numero specificato di processi worker.

Classe Lock: Quando molti processi utilizzano la stessa risorsa condivisa, è possibile evitare situazioni di competizione utilizzando la classe Lock per implementare l’esclusione reciproca.

Classi Value e Array: Queste classi consentono di creare oggetti condivisi che possono essere utilizzati da altri processi. Utili per il trasferimento sicuro di dati tra processi.

Classe Manager: I processi multipli possono accedere a oggetti condivisi e strutture dati create utilizzando la classe Manager. Fornisce astrazioni più complesse come spazi dei nomi, dizionari e liste.

Funzione Pipe:

La funzione Pipe() costruisce una coppia di oggetti di connessione per la comunicazione bidirezionale tra processi.

È possibile identificare il processo in esecuzione utilizzando l’oggetto corrente restituito da questa funzione.

Restituisce il numero di core CPU disponibili, che è utile per determinare quante attività eseguire contemporaneamente.

Creazione di processi utilizzando la classe Process

Puoi costruire e controllare diversi processi in Python utilizzando la classe Process del pacchetto multiprocessing. Ecco una spiegazione passo-passo su come creare processi utilizzando la classe Process e come fornire la funzione da eseguire in un nuovo processo utilizzando il parametro target:

import multiprocessing

# Esempio di funzione che verrà eseguita nel nuovo processo
def worker_function(numero):
    print(f"Il processo worker {numero} è in esecuzione")

if __name__ == "__main__":
    # Crea una lista di processi
    processi = []

    num_processi = 4

    for i in range(num_processi):
        # Crea un nuovo processo, specificando la funzione target e i suoi argomenti
        processo = multiprocessing.Process(target=worker_function, args=(i,))
        processi.append(processo)
        processo.start()  # Avvia il processo

    # Aspetta che tutti i processi terminino
    for processo in processi:
        processo.join()

    print("Tutti i processi sono terminati")

Il processo worker 0 è in esecuzione.

Il processo worker 1 è in esecuzione.

Il processo worker 2 è in esecuzione.

Il processo worker 3 è in esecuzione.

Tutti i processi sono terminati.

Comunicazione tra processi

Puoi costruire e controllare diversi processi in Python utilizzando la classe Process del pacchetto multiprocessing. Ecco una spiegazione passo-passo su come creare processi utilizzando la classe Process e come fornire la funzione da eseguire in un nuovo processo utilizzando il parametro target.

In un ambiente multi-processo, i processi possono sincronizzare le loro operazioni e condividere dati utilizzando varie tecniche e procedure conosciute come comunicazione tra processi (IPC). La comunicazione è fondamentale in un ambiente di multiprocessing, in cui numerosi processi operano contemporaneamente. Ciò consente ai processi di cooperare, condividere informazioni e pianificare le loro operazioni.

Metodi per l’IPC

Pipe:

I dati passano tra due processi utilizzando la struttura di IPC fondamentale nota come pipe. Mentre l’altro processo legge dalla pipe, il primo processo scrive dati. Le pipe possono essere denominate o anonime. Tuttavia, le pipe possono essere utilizzate solo per la comunicazione tra due processi distinti.

Code:

Le code del modulo multiprocessing offrono un metodo di IPC più flessibile. Consentono la comunicazione tra numerosi processi inviando messaggi attraverso la coda. Il processo trasmittente aggiunge i messaggi alla coda, mentre il processo ricevente li recupera. L’integrità dei dati e la sincronizzazione vengono gestite automaticamente tramite le code.

Memoria condivisa:

Permettendo l’accesso a più processi alla stessa area, la memoria condivisa facilita la condivisione e la comunicazione efficiente dei dati. Il controllo della memoria condivisa richiede una sincronizzazione precisa per evitare situazioni di concorrenza e garantire la coerenza dei dati.

Utilizzo delle code per la comunicazione

A causa della loro semplicità e della sincronizzazione integrata, le code sono una tecnica di IPC popolare nel modulo multiprocessing di Python. Ecco un esempio che mostra come utilizzare le code per la comunicazione tra processi:

import multiprocessing

# Funzione worker che inserisce dati nella coda
def produttore(coda):
    for i in range(5):
        coda.put(i)
        print(f"Prodotto: {i}")

# Funzione worker che recupera dati dalla coda
def consumatore(coda):
    while True:
        dati = coda.get()
        if dati is None:  # Valore sentinella per interrompere il ciclo
            break
        print(f"Consumato: {dati}")

if __name__ == "__main__":
    # Crea una coda per la comunicazione
    coda = multiprocessing.Queue()

    # Crea i processi produttore e consumatore
    processo_produttore = multiprocessing.Process(target=produttore, args=(coda,))
    processo_consumatore = multiprocessing.Process(target=consumatore, args=(coda,))

    # Avvia i processi
    processo_produttore.start()
    processo_consumatore.start()

    # Aspetta che il produttore termini
    processo_produttore.join()

    # Segnala al consumatore di fermarsi aggiungendo un valore sentinella alla coda
    coda.put(None)

    # Aspetta che il consumatore termini
    processo_consumatore.join()

    print("Tutti i processi sono terminati")

In questo caso, il processo produttore utilizza il metodo put() per aggiungere dati alla coda. Il processo consumatore recupera i dati dalla coda utilizzando il metodo get(). Una volta terminato il produttore, viene consigliato al consumatore di interrompersi utilizzando un valore sentinella (None). L’attesa per il completamento di entrambi i processi viene effettuata utilizzando la funzione join(). Questo esemplifica come le code offrano ai processi un metodo pratico e sicuro per scambiare dati senza tecniche esplicite di sincronizzazione.

Parallelismo con Pooling

Puoi parallelizzare l’esecuzione di una funzione su vari valori di input utilizzando la classe Pool nel modulo multiprocessing, che è uno strumento utile per gestire un pool di processi lavoratori. Semplifica l’assegnazione dei compiti e la raccolta dei risultati. Comunemente utilizzate per ottenere l’esecuzione parallela sono le operazioni map() e apply() della classe Pool.

Utilizzo di map() e apply() nella classe Pool

Funzione map():

Il metodo map() applica la funzione fornita a ciascun elemento di un iterabile e suddivide il carico tra i processi disponibili. Viene restituita una lista di risultati nello stesso ordine in cui sono stati inseriti i valori di input. Ecco un esempio:

import multiprocessing

def quadrato(numero):
    return numero ** 2

if __name__ == "__main__":
    dati_input = [1, 2, 3, 4, 5]

    with multiprocessing.Pool() as pool:
        risultati = pool.map(quadrato, dati_input)

    print("Risultati quadrati:", risultati)

Funzione apply():

Quando devi applicare una funzione a un singolo parametro su un pool di processi, utilizzi la funzione apply(). Restituisce il risultato dell’applicazione della funzione all’input. Ecco un esempio:

import multiprocessing

def cubo(numero):
    return numero ** 3

if __name__ == "__main__":
    numero = 4

    with multiprocessing.Pool() as pool:
        risultato = pool.apply(cubo, (numero,))

    print(f"{numero} al cubo è:", risultato)

Scenari in cui il Pooling migliora le prestazioni

Compiti legati alla CPU: La classe Pool può eseguire versioni parallele di compiti che richiedono molta potenza di calcolo della CPU, come simulazioni o calcoli. Sfruttando efficacemente più core della CPU, il carico può essere distribuito tra i compiti attivi.

Elaborazione dei dati: La classe Pool può gestire contemporaneamente molte componenti di un set di dati quando si tratta di compiti di elaborazione dei dati come trasformazione, filtraggio o analisi dei dati. Il tempo di elaborazione può essere significativamente ridotto come risultato.

Web scraping: La classe Pool può richiedere contemporaneamente dati da vari URL durante lo scraping di informazioni da più siti web. Questo velocizza il processo di raccolta dei dati.

Sincronizzazione e blocco: Quando due o più processi accedono contemporaneamente alle stesse risorse o variabili condivise in un sistema di multiprocessing, si verificano situazioni di competizione, con conseguente comportamento imprevedibile o non accurato. Le situazioni di competizione possono causare corruzione dei dati, crash e output del programma non accurato. L’integrità dei dati e le situazioni di competizione vengono evitate utilizzando tecniche di sincronizzazione come i blocchi.

Utilizzo dei blocchi per prevenire situazioni di competizione

La primitiva di sincronizzazione nota come “blocco” (abbreviazione di “mutua esclusione”) assicura che solo un processo possa accedere a un codice cruciale o a una risorsa condivisa in un determinato momento. Una volta che un processo ha un blocco, ha l’accesso esclusivo alla regione protetta e non può essere accessibile da altri processi fino al rilascio del blocco.

Richiedendo che i processi accedano alle risorse in modo sequenziale, i blocchi creano una forma di cooperazione che evita situazioni di competizione.

Esempi di blocchi utilizzati per proteggere l’integrità dei dati

import multiprocessing

def incrementa(contatore, blocco):
    for _ in range(100000):
        with blocco:
            contatore.value += 1

if __name__ == "__main__":
    contatore = multiprocessing.Value("i", 0)
    blocco = multiprocessing.Lock()

    processi = []

    for _ in range(4):
        processo = multiprocessing.Process(target=incrementa, args=(contatore, blocco))
        processi.append(processo)
        processo.start()

    for processo in processi:
        processo.join()

    print("Valore finale del contatore:", contatore.value)

Differenziare i compiti legati alla CPU e i compiti legati all’I/O

Compiti legati alla CPU: Un compito legato alla CPU utilizza ampiamente le capacità di elaborazione della CPU. Questi compiti richiedono notevoli risorse della CPU, inclusi calcoli complessi, operazioni matematiche, simulazioni ed elaborazione dei dati. I compiti legati alla CPU interagiscono raramente con risorse esterne come file e reti e trascorrono la maggior parte del loro tempo nell’esecuzione del codice.

Compiti legati all’I/O: I compiti legati all’I/O includono la lettura e la scrittura di file, l’invio di richieste attraverso le reti e la comunicazione con i database, tutto ciò richiede un notevole tempo di attesa per il completamento delle operazioni di I/O. Questi compiti trascorrono più tempo “in attesa” che nell’utilizzo attivo della CPU.

Gestione dei task bound CPU con i Process Pool

I process pool sono vantaggiosi per il controllo dei carichi di lavoro intensivi per la CPU. I process pool dividono i task bound CPU in numerosi processi in modo che possano essere eseguiti contemporaneamente su vari core della CPU perché, nella maggior parte dei casi, coinvolgono calcoli che possono essere parallelizzati. Ciò riduce considerevolmente il tempo di esecuzione e utilizza in modo efficace le risorse della CPU disponibili.

Utilizzando i process pool, è possibile garantire che i processori multi-core vengano utilizzati appieno per completare più rapidamente i task bound CPU. La classe Pool del modulo multiprocessing semplifica la creazione e la gestione di questi processi worker.

Programmazione asincrona per task bound I/O

La programmazione asincrona è una strategia appropriata per i lavori bound I/O, dove il principale collo di bottiglia è l’attesa delle operazioni di I/O (come la lettura/scrittura di file o le richieste di rete). Grazie al passaggio efficace tra le attività durante l’attesa dell’I/O, la programmazione asincrona consente a un singolo thread di gestire numerose attività contemporaneamente anziché utilizzare processi multipli.

Non è necessario configurare processi separati, come i process pool, durante l’utilizzo della programmazione asincrona. Invece, essa utilizza una strategia di multitasking cooperativo, in cui le attività rinunciano al controllo al ciclo degli eventi mentre attendono che si verifichi l’I/O in modo che altre attività possano continuare il loro lavoro. Ciò può migliorare significativamente la reattività delle applicazioni bound I/O.

Fattori che influenzano le prestazioni del multiprocessing

Diversi fattori influenzano le prestazioni delle soluzioni di multiprocessing:

  • Natura del task: I possibili vantaggi delle prestazioni del multiprocessing dipendono dal fatto che un lavoro sia bound CPU o bound I/O. Le operazioni bound I/O possono ottenere solo modesti benefici in termini di prestazioni a causa dell’attesa delle risorse esterne, ma i task bound CPU ne beneficiano di più poiché possono sfruttare più core.
  • Numero di core: L’accelerazione potenziale ottenuta dal multiprocessing dipende direttamente dal numero di core della CPU disponibili. Maggiore è l’esecuzione parallela, più incredibile è possibile. I processi devono coordinarsi e comunicare tra loro, il che aumenta il sovraccarico. Le code e altre tecniche di comunicazione efficaci aiutano a ridurre questo sovraccarico.
  • Granularità del task: Suddividere i lavori in pezzi più piccoli può aumentare il parallelismo e il bilanciamento del carico. Le attività molto dettagliate introducono sovraccarico di comunicazione.

Benchmark che confrontano le implementazioni

Ecco un confronto illustrativo di diverse implementazioni che utilizzano un semplice task bound CPU di calcolo dei fattoriali:

import time
import multiprocessing
import threading
import math

def factorial(n):
    return math.factorial(n)

def single_thread():
    for _ in range(4):
        factorial(5000)

def multi_thread():
    threads = []
    for _ in range(4):
        thread = threading.Thread(target=factorial, args=(5000,))
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()

def multi_process():
    processes = []
    for _ in range(4):
        process = multiprocessing.Process(target=factorial, args=(5000,))
        processes.append(process)
        process.start()
    for process in processes:
        process.join()

if __name__ == "__main__":
    start_time = time.time()
    single_thread()
    print("Single-threaded:", time.time() - start_time)

    start_time = time.time()
    multi_thread()
    print("Multi-threaded:", time.time() - start_time)

    start_time = time.time()
    multi_process()
    print("Multi-processing:", time.time() - start_time)

Affrontare il sovraccarico e i trade-off

Il multiprocessing ha svantaggi anche se può migliorare significativamente le prestazioni per i task bound CPU:

  • Sovraccarico di comunicazione: Nello sviluppo e nell’esecuzione dei processi può esserci un significativo sovraccarico di comunicazione, specialmente per operazioni semplici. È importante trovare un equilibrio tra il sovraccarico e il tempo di elaborazione.
  • Utilizzo della memoria: Poiché ogni processo ha la propria area di memoria, l’utilizzo della memoria può aumentare. È importante gestire attentamente la memoria.
  • Scalabilità: Sebbene il multiprocessing migliori le prestazioni sui sistemi multi-core, un parallelismo eccessivamente intenso potrebbe non comportare un aumento proporzionale della velocità a causa del sovraccarico di comunicazione.
  • Distribuzione dei task: Per un’esecuzione equilibrata, è essenziale suddividere efficacemente i lavori e gestire il carico di lavoro tra i processi.

Visualizzazione con Matplotlib

Una tecnica efficace per comprendere il comportamento e gli effetti del multiprocessing è la visualizzazione. È possibile seguire l’avanzamento dei processi, valutare i dati per vari scenari e mostrare visivamente i guadagni di prestazioni dalla parallelizzazione mediante la creazione di grafici e diagrammi.

Esempi di utilizzo di Matplotlib per la visualizzazione

Ecco due esempi di come è possibile utilizzare Matplotlib per visualizzare l’esecuzione e l’accelerazione del multiprocessing:

Esempio 1: Visualizzazione dell’esecuzione dei processi

Consideriamo uno scenario in cui stai elaborando un batch di immagini utilizzando processi multipli. Puoi visualizzare il progresso di ogni processo utilizzando un grafico a barre:

import multiprocessing
import time
import matplotlib.pyplot as plt

def process_image(image):
    time.sleep(2)  # Simulazione dell'elaborazione dell'immagine
    return f"Elaborata {image}"

if __name__ == "__main__":
    images = ["image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg"]
    num_processes = 4

    with multiprocessing.Pool(processes=num_processes) as pool:
        results = pool.map(process_image, images)

    plt.bar(range(len(images)), [1] * len(images), align="center", color="blue", 
    label="Elaborazione")
    plt.bar(range(len(results)), [1] * len(results), align="center", color="green", 
    label="Elaborate")

    plt.xticks(range(len(results)), images)
    plt.ylabel("Progresso")
    plt.title("Progresso dell'elaborazione delle immagini")
    plt.legend()

    plt.show()

Esempio 2: Confronto dell’accelerazione

import time
import threading
import multiprocessing
import matplotlib.pyplot as plt

def task():
    time.sleep(1)  # Simulazione del lavoro

def run_single_thread():
    for _ in range(4):
        task()

def run_multi_thread():
    threads = []
    for _ in range(4):
        thread = threading.Thread(target=task)
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()

def run_multi_process():
    processes = []
    for _ in range(4):
        process = multiprocessing.Process(target=task)
        processes.append(process)
        process.start()
    for process in processes:
        process.join()

if __name__ == "__main__":
    times = []

    start_time = time.time()
    run_single_thread()
    times.append(time.time() - start_time)

    start_time = time.time()
    run_multi_thread()
    times.append(time.time() - start_time)

    start_time = time.time()
    run_multi_process()
    times.append(time.time() - start_time)

    labels = ["Thread Singolo", "Thread Multipli", "Processi Multipli"]
    plt.bar(labels, times)
    plt.ylabel("Tempo di Esecuzione (s)")
    plt.title("Confronto dell'Accelerazione")

    plt.show()

Applicazione

In molti settori in cui i compiti possono essere suddivisi in unità di lavoro più piccole che possono essere completate in modo concorrente, il multiprocessing è vitale. Ecco alcuni scenari reali in cui il multiprocessing è cruciale:

  • Elaborazione dati: I processi mantengono una segregazione che impedisce la condivisione diretta della memoria. La comunicazione tra processi (IPC) è uno dei meccanismi più complessi per facilitare la comunicazione tra processi. Con le loro dimensioni sostanziali e l’isolamento intrinseco, i processi dimostrano una notevole competenza nella gestione di compiti intensivi in termini di risorse, come l’esecuzione di programmi indipendenti multipli.
  • Elaborazione di immagini e video: Il multiprocessing può aiutare ad applicare filtri, scalare e rilevare oggetti in immagini e video. Gestire ogni immagine o frame in parallelo per velocizzare le operazioni e consentire l’elaborazione in tempo reale nelle applicazioni video.

Il multiprocessing può velocizzare i processi di web scraping e crawling, raccogliendo dati da numerosi siti web. Raccolta e analisi dei dati utilizzando procedure multiple per recuperare informazioni da diverse fonti.

Deep learning e machine learning: L’utilizzo di enormi set di dati per addestrare modelli di machine learning spesso richiede attività computazionalmente intense. Utilizzare diversi core o GPU per le operazioni di dati e addestramento riduce il tempo di addestramento e migliora la convergenza del modello.

  • Calcolo parallelo e analisi numerica: Il multiprocessing è utile per calcoli matematici su larga scala, soluzioni di problemi complessi e simulazioni numeriche. Le computazioni parallele di matrici e le simulazioni di Monte Carlo sono due esempi di tecniche.

L’elaborazione a batch è necessaria per molte applicazioni, come la generazione di frame di animazione o l’elaborazione di rapporti aziendali. L’esecuzione parallela efficiente di queste attività avviene mediante multiprocessing.

Modellazione finanziaria

Complesse simulazioni finanziarie, analisi del rischio e modellazione degli scenari possono coinvolgere molti calcoli. Il multiprocessing accelera questi calcoli, consentendo decisioni e analisi più rapide.

Conclusioni

Esplorare le capacità di multiprocessing di Python ti offre la possibilità di modificare le prestazioni del tuo codice e velocizzare le applicazioni. Questo viaggio ha rivelato l’interazione complessa tra thread, processi e il modulo multiprocessing. Nuova vita grazie al multiprocessing, che offre efficienza e ottimizzazione. Ricorda che il multiprocessing è la tua chiave per l’innovazione, la velocità e l’efficienza mentre ci separiamo. Le tue nuove competenze acquisite ti preparano per progetti difficili, comprese simulazioni complesse e attività intensive di dati. Lascia che queste informazioni alimentino il tuo entusiasmo per la programmazione, spingendo le tue app verso una maggiore efficacia e impatto. Il viaggio continua, e ora che hai il multiprocessing a tua disposizione, le possibilità del tuo codice sono illimitate.

Punti chiave

  • La multiprogrammazione (multiprocessing) comporta l’esecuzione simultanea di più processi, consentendo ai programmi di sfruttare i moderni processori multi-core per una prestazione ottimale.
  • Processi: unità di esecuzione isolate con il proprio spazio di memoria, mentre i thread condividono la memoria all’interno di un processo. Comprendere le differenze aiuta a scegliere l’approccio di concorrenza giusto.
  • Il GIL (Global Interpreter Lock) di Python limita l’esecuzione parallela effettiva in scenari multithread, rendendo il multiprocessamento più adatto per compiti legati alla CPU che richiedono elaborazioni intensive.
  • I meccanismi di Comunicazione tra Processi (IPC), come pipe, code e memoria condivisa, consentono ai processi di comunicare e scambiare dati in modo sicuro.
  • La natura del compito, il numero di core, l’impatto del GIL, l’overhead di comunicazione, l’utilizzo della memoria e la granularità del compito influenzano le prestazioni del multiprocessamento. È necessaria una valutazione attenta per bilanciare l’utilizzo delle risorse e ottenere una scalabilità ottimale.

Domande frequenti

I media mostrati in questo articolo non sono di proprietà di Analytics Vidhya e vengono utilizzati a discrezione dell’autore.