Tutorial Python Multithreading e Multiprocessing

Pubblicato: 2022-03-11
Nota: a grande richiesta di dimostrare alcune tecniche alternative, tra cui async/await, disponibile solo dall'avvento di Python 3.5, ho aggiunto alcuni aggiornamenti alla fine dell'articolo. Divertiti!

Le discussioni che criticano Python parlano spesso di come sia difficile usare Python per il lavoro multithread, puntando il dito su quello che è noto come il blocco dell'interprete globale (denominato affettuosamente GIL) che impedisce l'esecuzione simultanea di più thread di codice Python. Per questo motivo, il modulo multithreading Python non si comporta come ci si aspetterebbe se non sei uno sviluppatore Python e provieni da altri linguaggi come C++ o Java. Deve essere chiarito che si può ancora scrivere codice in Python che viene eseguito contemporaneamente o in parallelo e fare una netta differenza nelle prestazioni risultanti, purché vengano prese in considerazione alcune cose. Se non l'hai ancora letto, ti suggerisco di dare un'occhiata all'articolo di Eqbal Quran su concorrenza e parallelismo in Ruby qui sul Blog di Toptal Engineering.

In questo tutorial sulla concorrenza Python, scriveremo un piccolo script Python per scaricare le immagini più popolari da Imgur. Inizieremo con una versione che scarica le immagini in sequenza o una alla volta. Come prerequisito, dovrai registrare un'applicazione su Imgur. Se non hai già un account Imgur, creane uno prima.

Gli script in questi esempi di threading sono stati testati con Python 3.6.4. Con alcune modifiche, dovrebbero anche essere eseguiti con Python 2: urllib è ciò che è cambiato di più tra queste due versioni di Python.

Guida introduttiva al multithreading di Python

Iniziamo creando un modulo Python, chiamato download.py . Questo file conterrà tutte le funzioni necessarie per recuperare l'elenco delle immagini e scaricarle. Divideremo queste funzionalità in tre funzioni separate:

  • get_links
  • download_link
  • setup_download_dir

La terza funzione, setup_download_dir , verrà utilizzata per creare una directory di destinazione del download se non esiste già.

L'API di Imgur richiede che le richieste HTTP contengano l'intestazione di Authorization con l'ID client. Puoi trovare questo ID client dalla dashboard dell'applicazione che hai registrato su Imgur e la risposta sarà codificata in JSON. Possiamo usare la libreria JSON standard di Python per decodificarla. Scaricare l'immagine è un'operazione ancora più semplice, poiché tutto ciò che devi fare è recuperare l'immagine dal suo URL e scriverla su un file.

Ecco come appare lo script:

 import json import logging import os from pathlib import Path from urllib.request import urlopen, Request logger = logging.getLogger(__name__) types = {'image/jpeg', 'image/png'} def get_links(client_id): headers = {'Authorization': 'Client-ID {}'.format(client_id)} req = Request('https://api.imgur.com/3/gallery/random/random/', headers=headers, method='GET') with urlopen(req) as resp: data = json.loads(resp.read().decode('utf-8')) return [item['link'] for item in data['data'] if 'type' in item and item['type'] in types] def download_link(directory, link): download_path = directory / os.path.basename(link) with urlopen(link) as image, download_path.open('wb') as f: f.write(image.read()) logger.info('Downloaded %s', link) def setup_download_dir(): download_dir = Path('images') if not download_dir.exists(): download_dir.mkdir() return download_dir

Successivamente, dovremo scrivere un modulo che utilizzerà queste funzioni per scaricare le immagini, una per una. Chiameremo questo single.py . Questo conterrà la funzione principale della nostra prima versione ingenua del downloader di immagini Imgur. Il modulo recupererà l'ID client Imgur nella variabile di ambiente IMGUR_CLIENT_ID . setup_download_dir per creare la directory di destinazione del download. Infine, recupererà un elenco di immagini utilizzando la funzione get_links , filtrerà tutte le GIF e gli URL degli album, quindi utilizzerà download_link per scaricare e salvare ciascuna di queste immagini sul disco. Ecco come appare single.py :

 import logging import os from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) for link in links: download_link(download_dir, link) logging.info('Took %s seconds', time() - ts) if __name__ == '__main__': main()

Sul mio laptop, questo script ha impiegato 19,4 secondi per scaricare 91 immagini. Tieni presente che questi numeri possono variare in base alla rete in cui ti trovi. 19,4 secondi non sono molto lunghi, ma se volessimo scaricare più immagini? Forse 900 immagini, invece di 90. Con una media di 0,2 secondi per immagine, 900 immagini richiederebbero circa 3 minuti. Per 9000 immagini ci vorrebbero 30 minuti. La buona notizia è che introducendo concorrenza o parallelismo, possiamo accelerare notevolmente questo processo.

Tutti gli esempi di codice successivi mostreranno solo istruzioni di importazione nuove e specifiche per quegli esempi. Per comodità, tutti questi script Python possono essere trovati in questo repository GitHub.

Concorrenza e parallelismo in Python: esempio di threading

Il threading è uno degli approcci più noti per ottenere la concorrenza e il parallelismo di Python. Il threading è una funzionalità solitamente fornita dal sistema operativo. I thread sono più leggeri dei processi e condividono lo stesso spazio di memoria.

Modello di memoria multithreading Python

In questo esempio di threading Python, scriveremo un nuovo modulo per sostituire single.py . Questo modulo creerà un pool di otto thread, per un totale di nove thread incluso il thread principale. Ho scelto otto thread di lavoro perché il mio computer ha otto core CPU e un thread di lavoro per core sembrava un buon numero per quanti thread eseguire contemporaneamente. In pratica, questo numero viene scelto con molta più attenzione in base ad altri fattori, come altre applicazioni e servizi in esecuzione sulla stessa macchina.

È quasi uguale alla precedente, con l'eccezione che ora abbiamo una nuova classe, DownloadWorker , che è una discendente della classe Python Thread . Il metodo run è stato sovrascritto, che esegue un ciclo infinito. Ad ogni iterazione, chiama self.queue.get() per provare a recuperare un URL da una coda thread-safe. Si blocca finché non c'è un elemento nella coda che il lavoratore deve elaborare. Una volta che il lavoratore riceve un elemento dalla coda, chiama lo stesso metodo download_link utilizzato nello script precedente per scaricare l'immagine nella directory images. Al termine del download, il lavoratore segnala alla coda che l'attività è stata eseguita. Questo è molto importante, perché la coda tiene traccia di quante attività sono state accodate. La chiamata a queue.join() bloccherebbe per sempre il thread principale se i lavoratori non segnalano di aver completato un'attività.

 import logging import os from queue import Queue from threading import Thread from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class DownloadWorker(Thread): def __init__(self, queue): Thread.__init__(self) self.queue = queue def run(self): while True: # Get the work from the queue and expand the tuple directory, link = self.queue.get() try: download_link(directory, link) finally: self.queue.task_done() def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) # Create a queue to communicate with the worker threads queue = Queue() # Create 8 worker threads for x in range(8): worker = DownloadWorker(queue) # Setting daemon to True will let the main thread exit even though the workers are blocking worker.daemon = True worker.start() # Put the tasks into the queue as a tuple for link in links: logger.info('Queueing {}'.format(link)) queue.put((download_dir, link)) # Causes the main thread to wait for the queue to finish processing all the tasks queue.join() logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

L'esecuzione di questo script di esempio di threading Python sulla stessa macchina utilizzata in precedenza comporta un tempo di download di 4,1 secondi! È 4,7 volte più veloce dell'esempio precedente. Anche se questo è molto più veloce, vale la pena ricordare che solo un thread alla volta era in esecuzione durante questo processo a causa del GIL. Pertanto, questo codice è simultaneo ma non parallelo. Il motivo per cui è ancora più veloce è perché si tratta di un'attività legata all'IO. Il processore non fa fatica a sudare durante il download di queste immagini e la maggior parte del tempo viene spesa in attesa della rete. Questo è il motivo per cui il multithreading Python può fornire un grande aumento di velocità. Il processore può passare da un thread all'altro ogni volta che uno di essi è pronto per eseguire un po' di lavoro. L'uso del modulo di threading in Python o qualsiasi altro linguaggio interpretato con un GIL può effettivamente comportare prestazioni ridotte. Se il tuo codice sta eseguendo un'attività legata alla CPU, come la decompressione di file gzip, l'uso del modulo di threading comporterà un tempo di esecuzione più lento. Per le attività legate alla CPU e l'esecuzione veramente parallela, possiamo utilizzare il modulo multiprocessing.

Sebbene l'implementazione Python di riferimento de facto , CPython, abbia un GIL, questo non è vero per tutte le implementazioni Python. Ad esempio, IronPython, un'implementazione Python che utilizza il framework .NET, non ha un GIL e nemmeno Jython, l'implementazione basata su Java. Puoi trovare un elenco di implementazioni Python funzionanti qui.

Correlati: Best practice e suggerimenti per Python di Toptal Developers

Concorrenza e parallelismo in Python Esempio 2: generazione di più processi

Il modulo multiprocessing è più facile da inserire rispetto al modulo threading, poiché non è necessario aggiungere una classe come l'esempio di threading Python. Le uniche modifiche che dobbiamo apportare sono nella funzione principale.

Tutorial multiprocessing Python: Moduli

Per utilizzare più processi, creiamo un Pool multiprocessing. Con il metodo map che fornisce, passeremo l'elenco di URL al pool, che a sua volta genererà otto nuovi processi e utilizzerà ciascuno di essi per scaricare le immagini in parallelo. Questo è vero parallelismo, ma ha un costo. L'intera memoria dello script viene copiata in ogni processo secondario generato. In questo semplice esempio, non è un grosso problema, ma può facilmente diventare un serio sovraccarico per programmi non banali.

 import logging import os from functools import partial from multiprocessing.pool import Pool from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.getLogger('requests').setLevel(logging.CRITICAL) logger = logging.getLogger(__name__) def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) download = partial(download_link, download_dir) with Pool(4) as p: p.map(download, links) logging.info('Took %s seconds', time() - ts) if __name__ == '__main__': main()

Concorrenza e parallelismo in Python Esempio 3: distribuzione a più lavoratori

Mentre i moduli di threading e multiprocessing sono ottimi per gli script in esecuzione sul tuo personal computer, cosa dovresti fare se vuoi che il lavoro venga svolto su una macchina diversa, o se hai bisogno di scalare fino a più di quanto la CPU su una macchina possa fare maniglia? Un ottimo caso d'uso per questo sono le attività di back-end di lunga durata per le applicazioni Web. Se hai alcune attività di lunga durata, non vuoi far girare un mucchio di sottoprocessi o thread sulla stessa macchina che deve eseguire il resto del codice dell'applicazione. Ciò degraderà le prestazioni della tua applicazione per tutti i tuoi utenti. Sarebbe fantastico poter eseguire questi lavori su un'altra macchina o su molte altre macchine.

Un'ottima libreria Python per questo compito è RQ, una libreria molto semplice ma potente. Per prima cosa accodi una funzione e i suoi argomenti usando la libreria. Questo seleziona la rappresentazione della chiamata di funzione, che viene quindi aggiunta a un elenco Redis. L'accodamento del lavoro è il primo passo, ma non farà ancora nulla. Abbiamo anche bisogno che almeno un lavoratore ascolti su quella coda di lavoro.

Modello della libreria di code RQ Python

Il primo passaggio consiste nell'installare ed eseguire un server Redis sul computer o accedere a un server Redis in esecuzione. Dopodiché, ci sono solo alcune piccole modifiche apportate al codice esistente. Per prima cosa creiamo un'istanza di una coda RQ e le passiamo un'istanza di un server Redis dalla libreria redis-py. Quindi, invece di chiamare semplicemente il nostro metodo download_link , chiamiamo q.enqueue(download_link, download_dir, link) . Il metodo enqueue accetta una funzione come primo argomento, quindi qualsiasi altro argomento o argomento della parola chiave viene passato a quella funzione quando il lavoro viene effettivamente eseguito.

Un ultimo passo che dobbiamo fare è avviare alcuni lavoratori. RQ fornisce un pratico script per eseguire i lavoratori sulla coda predefinita. Basta eseguire rqworker in una finestra di terminale e avvierà un worker in ascolto sulla coda predefinita. Assicurati che la tua directory di lavoro corrente sia la stessa in cui risiedono gli script. Se vuoi ascoltare una coda diversa, puoi eseguire rqworker queue_name e ascolterà quella coda denominata. La cosa grandiosa di RQ è che finché puoi connetterti a Redis, puoi eseguire tutti i lavoratori che vuoi su tutte le macchine che vuoi; pertanto, è molto facile aumentare la scalabilità man mano che l'applicazione cresce. Ecco la fonte per la versione RQ:

 import logging import os from redis import Redis from rq import Queue from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.getLogger('requests').setLevel(logging.CRITICAL) logger = logging.getLogger(__name__) def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) q = Queue(connection=Redis(host='localhost', port=6379)) for link in links: q.enqueue(download_link, download_dir, link) if __name__ == '__main__': main()

Tuttavia, RQ non è l'unica soluzione Python per la coda dei lavori. RQ è facile da usare e copre molto bene casi d'uso semplici, ma se sono necessarie opzioni più avanzate, è possibile utilizzare altre soluzioni di coda Python 3 (come Celery).

Python Multithreading vs. Multiprocessing

Se il tuo codice è legato a IO, sia il multiprocessing che il multithreading in Python funzioneranno per te. Il multiprocessing è più semplice da inserire rispetto al threading, ma ha un sovraccarico di memoria più elevato. Se il tuo codice è vincolato alla CPU, molto probabilmente il multiprocessing sarà la scelta migliore, specialmente se la macchina di destinazione ha più core o CPU. Per le applicazioni Web e quando è necessario ridimensionare il lavoro su più macchine, RQ sarà la soluzione migliore per te.

Relazionato: Diventa più avanzato: evita i 10 errori più comuni che fanno i programmatori Python

Aggiornare

Python concurrent.futures

Qualcosa di nuovo da Python 3.2 che non è stato toccato nell'articolo originale è il pacchetto concurrent.futures . Questo pacchetto fornisce ancora un altro modo per usare la concorrenza e il parallelismo con Python.

Nell'articolo originale, ho menzionato che il modulo multiprocessing di Python sarebbe più facile da inserire nel codice esistente rispetto al modulo di threading. Questo perché il modulo di threading di Python 3 richiedeva la sottoclasse della classe Thread e anche la creazione di una Queue per il monitoraggio del lavoro dei thread.

L'uso di un concurrent.futures.ThreadPoolExecutor rende il codice di esempio di threading Python quasi identico al modulo multiprocessing.

 import logging import os from concurrent.futures import ThreadPoolExecutor from functools import partial from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) # By placing the executor inside a with block, the executors shutdown method # will be called cleaning up threads. # # By default, the executor sets number of workers to 5 times the number of # CPUs. with ThreadPoolExecutor() as executor: # Create a new partially applied function that stores the directory # argument. # # This allows the download_link function that normally takes two # arguments to work with the map function that expects a function of a # single argument. fn = partial(download_link, download_dir) # Executes fn concurrently using threads on the links iterable. The # timeout is for the entire process, not a single call, so downloading # all images must complete within 30 seconds. executor.map(fn, links, timeout=30) if __name__ == '__main__': main()

Ora che abbiamo scaricato tutte queste immagini con il nostro Python ThreadPoolExecutor , possiamo usarle per testare un'attività legata alla CPU. Possiamo creare versioni in miniatura di tutte le immagini sia in uno script a thread singolo che a processo singolo e quindi testare una soluzione basata su multiprocessing.

Utilizzeremo la libreria Pillow per gestire il ridimensionamento delle immagini.

Ecco il nostro copione iniziale.

 import logging from pathlib import Path from time import time from PIL import Image logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def create_thumbnail(size, path): """ Creates a thumbnail of an image with the same name as image but with _thumbnail appended before the extension. Eg: >>> create_thumbnail((128, 128), 'image.jpg') A new thumbnail image is created with the name image_thumbnail.jpg :param size: A tuple of the width and height of the image :param path: The path to the image file :return: None """ image = Image.open(path) image.thumbnail(size) path = Path(path) name = path.stem + '_thumbnail' + path.suffix thumbnail_path = path.with_name(name) image.save(thumbnail_path) def main(): ts = time() for image_path in Path('images').iterdir(): create_thumbnail((128, 128), image_path) logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

Questo script esegue un'iterazione sui percorsi nella cartella delle images e per ogni percorso esegue la funzione create_thumbnail. Questa funzione utilizza Pillow per aprire l'immagine, creare una miniatura e salvare la nuova immagine più piccola con lo stesso nome dell'originale ma con _thumbnail aggiunto al nome.

L'esecuzione di questo script su 160 immagini per un totale di 36 milioni richiede 2,32 secondi. Vediamo se possiamo velocizzarlo usando un ProcessPoolExecutor.

 import logging from pathlib import Path from time import time from functools import partial from concurrent.futures import ProcessPoolExecutor from PIL import Image logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def create_thumbnail(size, path): """ Creates a thumbnail of an image with the same name as image but with _thumbnail appended before the extension. Eg: >>> create_thumbnail((128, 128), 'image.jpg') A new thumbnail image is created with the name image_thumbnail.jpg :param size: A tuple of the width and height of the image :param path: The path to the image file :return: None """ path = Path(path) name = path.stem + '_thumbnail' + path.suffix thumbnail_path = path.with_name(name) image = Image.open(path) image.thumbnail(size) image.save(thumbnail_path) def main(): ts = time() # Partially apply the create_thumbnail method, setting the size to 128x128 # and returning a function of a single argument. thumbnail_128 = partial(create_thumbnail, (128, 128)) # Create the executor in a with block so shutdown is called when the block # is exited. with ProcessPoolExecutor() as executor: executor.map(thumbnail_128, Path('images').iterdir()) logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

Il metodo create_thumbnail è identico all'ultimo script. La differenza principale è la creazione di un ProcessPoolExecutor . Il metodo della mappa dell'esecutore viene utilizzato per creare le miniature in parallelo. Per impostazione predefinita, ProcessPoolExecutor crea un sottoprocesso per CPU. L'esecuzione di questo script sulle stesse 160 immagini ha richiesto 1,05 secondi, 2,2 volte più velocemente!

Asincrono/In attesa (solo Python 3.5+)

Uno degli elementi più richiesti nei commenti all'articolo originale era per un esempio l'utilizzo del modulo asyncio di Python 3. Rispetto agli altri esempi, c'è una nuova sintassi Python che potrebbe essere nuova per la maggior parte delle persone e anche alcuni nuovi concetti. Uno sfortunato livello aggiuntivo di complessità è causato dal fatto che il modulo urllib integrato in Python non è asincrono. Avremo bisogno di utilizzare una libreria HTTP asincrona per ottenere tutti i vantaggi di asyncio. Per questo, useremo aiohttp.

Entriamo subito nel codice e seguirà una spiegazione più dettagliata.

 import asyncio import logging import os from time import time import aiohttp from download import setup_download_dir, get_links logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) async def async_download_link(session, directory, link): """ Async version of the download_link method we've been using in the other examples. :param session: aiohttp ClientSession :param directory: directory to save downloads :param link: the url of the link to download :return: """ download_path = directory / os.path.basename(link) async with session.get(link) as response: with download_path.open('wb') as f: while True: # await pauses execution until the 1024 (or less) bytes are read from the stream chunk = await response.content.read(1024) if not chunk: # We are done reading the file, break out of the while loop break f.write(chunk) logger.info('Downloaded %s', link) # Main is now a coroutine async def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() # We use a session to take advantage of tcp keep-alive # Set a 3 second read and connect timeout. Default is 5 minutes async with aiohttp.ClientSession(conn_timeout=3, read_timeout=3) as session: tasks = [(async_download_link(session, download_dir, l)) for l in get_links(client_id)] # gather aggregates all the tasks and schedules them in the event loop await asyncio.gather(*tasks, return_exceptions=True) if __name__ == '__main__': ts = time() # Create the asyncio event loop loop = asyncio.get_event_loop() try: loop.run_until_complete(main()) finally: # Shutdown the loop even if there is an exception loop.close() logger.info('Took %s seconds to complete', time() - ts)

C'è un bel po' da disfare qui. Iniziamo con il punto di ingresso principale del programma. La prima cosa che facciamo con il modulo asyncio è ottenere il ciclo degli eventi. Il ciclo di eventi gestisce tutto il codice asincrono. Quindi, il ciclo viene eseguito fino al completamento e viene passata la funzione main . C'è una nuova sintassi nella definizione di main: async def . Noterai anche await e with async .

La sintassi asincrona/attesa è stata introdotta in PEP492. La sintassi async def contrassegna una funzione come coroutine. Internamente, le coroutine sono basate su generatori Python, ma non sono esattamente la stessa cosa. Le coroutine restituiscono un oggetto coroutine simile a come i generatori restituiscono un oggetto generatore. Una volta che hai una coroutine, ottieni i suoi risultati con l'espressione await . Quando una coroutine chiama in await , l'esecuzione della coroutine viene sospesa fino al completamento dell'attesa. Questa sospensione permette di portare a termine altri lavori mentre la coroutine è sospesa “in attesa” di qualche risultato. In generale, questo risultato sarà una sorta di I/O come una richiesta di database o nel nostro caso una richiesta HTTP.

La funzione download_link doveva essere modificata in modo abbastanza significativo. In precedenza, ci affidavamo a urllib per fare il peso maggiore del lavoro di lettura dell'immagine per noi. Ora, per consentire al nostro metodo di funzionare correttamente con il paradigma di programmazione asincrona, abbiamo introdotto un ciclo while che legge blocchi dell'immagine alla volta e sospende l'esecuzione in attesa del completamento dell'I/O. Ciò consente al loop di eventi di scorrere il download delle diverse immagini poiché ognuna ha nuovi dati disponibili durante il download.

Dovrebbe essercene uno, preferibilmente uno solo, modo ovvio per farlo

Mentre lo zen di Python ci dice che dovrebbe esserci un modo ovvio per fare qualcosa, ci sono molti modi in Python per introdurre la concorrenza nei nostri programmi. Il metodo migliore da scegliere dipenderà dal tuo caso d'uso specifico. Il paradigma asincrono si adatta meglio ai carichi di lavoro ad alta concorrenza (come un server Web) rispetto al threading o al multiprocessing, ma richiede che il codice (e le dipendenze) siano asincroni per trarne pieno vantaggio.

Si spera che gli esempi di threading Python in questo articolo, e l'aggiornamento, ti indichino la giusta direzione in modo da avere un'idea di dove cercare nella libreria standard di Python se devi introdurre la concorrenza nei tuoi programmi.