Tutorial Python Multithreading și Multiprocessing
Publicat: 2022-03-11Discuțiile care critică Python vorbesc adesea despre cum este dificil să utilizați Python pentru lucrul cu mai multe fire, arătând cu degetul spre ceea ce este cunoscut sub numele de blocare globală a interpretului (denumită cu afecțiune GIL) care împiedică rularea simultană a mai multor fire de cod Python. Din această cauză, modulul Python multithreading nu se comportă așa cum v-ați aștepta dacă nu sunteți un dezvoltator Python și veniți din alte limbaje, cum ar fi C++ sau Java. Trebuie să fie clar că se poate scrie cod în Python care rulează simultan sau în paralel și să facă o diferență majoră în performanța rezultată, atâta timp cât anumite lucruri sunt luate în considerare. Dacă nu ați citit-o încă, vă sugerez să aruncați o privire la articolul lui Eqbal Quran despre concurență și paralelism în Ruby aici pe Toptal Engineering Blog.
În acest tutorial de concurență Python, vom scrie un mic script Python pentru a descărca cele mai populare imagini din Imgur. Vom începe cu o versiune care descarcă imaginile secvenţial, sau pe rând. Ca o condiție prealabilă, va trebui să înregistrați o aplicație pe Imgur. Dacă nu aveți deja un cont Imgur, vă rugăm să creați unul mai întâi.
Scripturile din aceste exemple de threading au fost testate cu Python 3.6.4. Cu unele modificări, ar trebui să ruleze și cu Python 2 — urllib este ceea ce s-a schimbat cel mai mult între aceste două versiuni de Python.
Noțiuni introductive cu Python Multithreading
Să începem prin a crea un modul Python, numit download.py
. Acest fișier va conține toate funcțiile necesare pentru a prelua lista de imagini și a le descărca. Vom împărți aceste funcționalități în trei funcții separate:
-
get_links
-
download_link
-
setup_download_dir
A treia funcție, setup_download_dir
, va fi folosită pentru a crea un director de destinație de descărcare dacă nu există deja.
API-ul Imgur necesită solicitări HTTP să poarte antetul de Authorization
cu ID-ul clientului. Puteți găsi acest ID de client din tabloul de bord al aplicației pe care ați înregistrat-o pe Imgur, iar răspunsul va fi codificat JSON. Putem folosi biblioteca standard JSON a lui Python pentru a o decoda. Descărcarea imaginii este o sarcină și mai simplă, deoarece tot ce trebuie să faceți este să preluați imaginea după adresa URL și să o scrieți într-un fișier.
Iată cum arată scriptul:
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
În continuare, va trebui să scriem un modul care să folosească aceste funcții pentru a descărca imaginile, una câte una. Vom numi acest single.py
. Aceasta va conține funcția principală a primei noastre versiuni naive a programului de descărcare a imaginilor Imgur. Modulul va prelua ID-ul clientului Imgur din variabila de mediu IMGUR_CLIENT_ID
. Acesta va invoca setup_download_dir
pentru a crea directorul de destinație a descărcarii. În cele din urmă, va prelua o listă de imagini folosind funcția get_links
, va filtra toate URL-urile GIF și ale albumelor, apoi va folosi download_link
pentru a descărca și salva fiecare dintre acele imagini pe disc. Iată cum arată 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()
Pe laptopul meu, acest script a durat 19,4 secunde pentru a descărca 91 de imagini. Vă rugăm să rețineți că aceste numere pot varia în funcție de rețeaua în care vă aflați. 19,4 secunde nu este îngrozitor de lung, dar dacă am vrea să descarcăm mai multe imagini? Poate 900 de imagini, în loc de 90. Cu o medie de 0,2 secunde pe imagine, 900 de imagini ar dura aproximativ 3 minute. Pentru 9000 de poze ar dura 30 de minute. Vestea bună este că prin introducerea concurenței sau paralelismului, putem accelera acest lucru dramatic.
Toate exemplele de cod ulterioare vor afișa numai instrucțiunile de import care sunt noi și specifice acestor exemple. Pentru comoditate, toate aceste scripturi Python pot fi găsite în acest depozit GitHub.
Concurență și paralelism în Python: Exemplu de threading
Threadingul este una dintre cele mai cunoscute abordări pentru a obține concurența și paralelismul Python. Threadingul este o caracteristică oferită de obicei de sistemul de operare. Firele sunt mai ușoare decât procesele și împart același spațiu de memorie.
În acest exemplu de threading Python, vom scrie un nou modul pentru a înlocui single.py
. Acest modul va crea un grup de opt fire, făcând un total de nouă fire, inclusiv firul principal. Am ales opt fire de lucru pentru că computerul meu are opt nuclee CPU și un fir de lucru pe nucleu mi s-a părut un număr bun pentru câte fire de execuție trebuie să ruleze simultan. În practică, acest număr este ales mult mai atent pe baza altor factori, cum ar fi alte aplicații și servicii care rulează pe aceeași mașină.
Aceasta este aproape aceeași cu cea anterioară, cu excepția că acum avem o nouă clasă, DownloadWorker
, care este un descendent al clasei Python Thread
. Metoda de rulare a fost suprascrisă, care rulează o buclă infinită. La fiecare iterație, apelează self.queue.get()
pentru a încerca să preia o adresă URL dintr-o coadă sigură pentru fire. Se blochează până când există un articol în coadă pe care lucrătorul îl poate procesa. Odată ce lucrătorul primește un articol din coadă, apelează apoi aceeași metodă download_link
care a fost folosită în scriptul anterior pentru a descărca imaginea în directorul de imagini. După ce descărcarea s-a terminat, lucrătorul semnalează coada că sarcina respectivă este finalizată. Acest lucru este foarte important, deoarece Coada ține evidența câte sarcini au fost puse în coadă. Apelul la queue.join()
ar bloca pentru totdeauna firul principal dacă lucrătorii nu au semnalat că au finalizat o sarcină.
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()
Rularea acestui exemplu de script de threading Python pe aceeași mașină folosită mai devreme are ca rezultat un timp de descărcare de 4,1 secunde! Este de 4,7 ori mai rapid decât exemplul anterior. Deși acest lucru este mult mai rapid, merită menționat că doar un fir de execuție s-a executat la un moment dat pe parcursul acestui proces datorită GIL. Prin urmare, acest cod este concurent, dar nu paralel. Motivul pentru care este încă mai rapid este că aceasta este o sarcină legată de IO. Procesorul cu greu transpira în timp ce descarcă aceste imagini, iar cea mai mare parte a timpului este petrecut în așteptarea rețelei. Acesta este motivul pentru care multithreadingul Python poate oferi o creștere mare a vitezei. Procesorul poate comuta între fire de execuție ori de câte ori unul dintre ele este gata să lucreze. Utilizarea modulului de threading în Python sau orice alt limbaj interpretat cu un GIL poate duce de fapt la o performanță redusă. Dacă codul dvs. realizează o sarcină legată de CPU, cum ar fi decomprimarea fișierelor gzip, utilizarea modulului de threading
va duce la un timp de execuție mai lent. Pentru sarcini legate de CPU și execuție cu adevărat paralelă, putem folosi modulul de multiprocesare.
În timp ce implementarea Python de referință de facto —CPython—are un GIL, acest lucru nu este valabil pentru toate implementările Python. De exemplu, IronPython, o implementare Python care utilizează framework-ul .NET, nu are un GIL și nici Jython, implementarea bazată pe Java. Puteți găsi o listă de implementări Python funcționale aici.
Concurență și paralelism în Python Exemplul 2: Generarea mai multor procese
Modulul de multiprocesare este mai ușor de introdus decât modulul de threading, deoarece nu este nevoie să adăugăm o clasă precum exemplul de threading Python. Singurele modificări pe care trebuie să le facem sunt în funcția principală.
Pentru a utiliza mai multe procese, creăm un Pool
de procesare multiplă. Cu metoda hărții pe care o oferă, vom trece lista de URL-uri către pool, care, la rândul său, va genera opt noi procese și le va folosi pe fiecare pentru a descărca imaginile în paralel. Acesta este paralelism adevărat, dar are un cost. Întreaga memorie a scriptului este copiată în fiecare subproces care este generat. În acest exemplu simplu, nu este mare lucru, dar poate deveni cu ușurință o suprasolicitare serioasă pentru programele non-triviale.
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()
Concurență și paralelism în Python Exemplul 3: Distribuirea către mai mulți lucrători
În timp ce modulele de threading și multiprocesare sunt grozave pentru scripturile care rulează pe computerul personal, ce ar trebui să faceți dacă doriți ca munca să fie efectuată pe o altă mașină sau trebuie să scalați la mai mult decât poate CPU-ul de pe o singură mașină mâner? Un caz de utilizare excelent pentru aceasta este sarcinile back-end de lungă durată pentru aplicațiile web. Dacă aveți niște sarcini de lungă durată, nu doriți să porniți o grămadă de sub-procese sau fire de execuție pe aceeași mașină care trebuie să ruleze restul codului aplicației. Acest lucru va degrada performanța aplicației dvs. pentru toți utilizatorii dvs. Ceea ce ar fi grozav este să poți rula aceste joburi pe o altă mașină sau pe multe alte mașini.
O bibliotecă Python grozavă pentru această sarcină este RQ, o bibliotecă foarte simplă, dar puternică. Mai întâi puneți în coadă o funcție și argumentele acesteia folosind biblioteca. Aceasta decupează reprezentarea apelului de funcție, care este apoi atașată la o listă Redis. Punerea în coadă a jobului este primul pas, dar nu va face nimic încă. De asemenea, avem nevoie de cel puțin un muncitor care să asculte la coada respectivă de locuri de muncă.
Primul pas este să instalați și să rulați un server Redis pe computer sau să aveți acces la un server Redis care rulează. După aceea, există doar câteva mici modificări aduse codului existent. Mai întâi creăm o instanță a unei cozi RQ și îi transmitem o instanță a unui server Redis din biblioteca redis-py. Apoi, în loc să apelăm doar metoda noastră download_link
, numim q.enqueue(download_link, download_dir, link)
. Metoda enqueue ia o funcție ca prim argument, apoi orice alte argumente sau argumente ale cuvintelor cheie sunt transmise acelei funcție atunci când jobul este executat efectiv.

Un ultim pas pe care trebuie să-l facem este să înființăm niște muncitori. RQ oferă un script la îndemână pentru a rula lucrătorii în coada implicită. Doar rulați rqworker
într-o fereastră de terminal și va porni un lucrător care ascultă în coada implicită. Vă rugăm să vă asigurați că directorul de lucru curent este același cu cel în care se află scripturile. Dacă doriți să ascultați o altă coadă, puteți rula rqworker queue_name
și va asculta coada numită. Lucrul grozav despre RQ este că atâta timp cât vă puteți conecta la Redis, puteți rula cât de mulți lucrători doriți pe câte mașini diferite doriți; prin urmare, este foarte ușor să se extindă pe măsură ce aplicația dvs. crește. Iată sursa pentru versiunea 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()
Cu toate acestea, RQ nu este singura soluție Python pentru coada de joburi. RQ este ușor de utilizat și acoperă extrem de bine cazurile simple de utilizare, dar dacă sunt necesare opțiuni mai avansate, pot fi utilizate și alte soluții de coadă Python 3 (cum ar fi țelina).
Python Multithreading vs. Multiprocesare
Dacă codul dvs. este legat de IO, atât multiprocesarea, cât și multithreadingul în Python vor funcționa pentru dvs. Multiprocesarea este mai ușor de introdus decât threading, dar are o supraîncărcare a memoriei mai mare. Dacă codul dvs. este legat de CPU, multiprocesarea va fi cea mai bună alegere, mai ales dacă mașina țintă are mai multe nuclee sau procesoare. Pentru aplicațiile web și atunci când trebuie să scalați munca pe mai multe mașini, RQ va fi mai bun pentru dvs.
Actualizați
Python concurrent.futures
Ceva nou de la Python 3.2 care nu a fost atins în articolul original este pachetul concurrent.futures
. Acest pachet oferă încă o modalitate de a utiliza concurența și paralelismul cu Python.
În articolul original, am menționat că modulul de multiprocesare al lui Python ar fi mai ușor de introdus în codul existent decât modulul de threading. Acest lucru se datorează faptului că modulul de threading Python 3 a necesitat subclasarea clasei Thread
și, de asemenea, crearea unei Queue
pentru ca firele de execuție să le monitorizeze.
Utilizarea unui concurrent.futures.ThreadPoolExecutor face ca exemplul de cod de threading Python să fie aproape identic cu modulul de multiprocesare.
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()
Acum că avem toate aceste imagini descărcate cu ajutorul nostru Python ThreadPoolExecutor
, le putem folosi pentru a testa o sarcină legată de CPU. Putem crea versiuni în miniatură ale tuturor imaginilor atât într-un script cu un singur thread, cu un singur proces și apoi să testăm o soluție bazată pe multiprocesare.
Vom folosi biblioteca Pillow pentru a gestiona redimensionarea imaginilor.
Iată scenariul nostru inițial.
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()
Acest script iterează peste căile din folderul images
și pentru fiecare cale rulează funcția create_thumbnail. Această funcție folosește Pillow pentru a deschide imaginea, a crea o miniatură și a salva imaginea nouă, mai mică, cu același nume ca originalul, dar cu _thumbnail
atașată la nume.
Rularea acestui script pe 160 de imagini cu un total de 36 de milioane durează 2,32 secunde. Să vedem dacă putem accelera acest lucru folosind 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()
Metoda create_thumbnail
este identică cu ultimul script. Principala diferență este crearea unui ProcessPoolExecutor
. Metoda hărții executantului este folosită pentru a crea miniaturile în paralel. În mod implicit, ProcessPoolExecutor
creează un subproces per CPU. Rularea acestui script pe aceleași 160 de imagini a durat 1,05 secunde — de 2,2 ori mai rapid!
Async/Await (numai Python 3.5+)
Unul dintre cele mai solicitate articole din comentariile la articolul original a fost, de exemplu, utilizarea modulului asincron al lui Python 3. În comparație cu celelalte exemple, există o nouă sintaxă Python care poate fi nouă pentru majoritatea oamenilor și, de asemenea, unele concepte noi. Un strat suplimentar nefericit de complexitate este cauzat de faptul că modulul urllib
încorporat al lui Python nu este asincron. Va trebui să folosim o bibliotecă HTTP asincronă pentru a obține toate beneficiile asyncio. Pentru aceasta, vom folosi aiohttp.
Să intrăm direct în cod și va urma o explicație mai detaliată.
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)
Este destul de mult de despachetat aici. Să începem cu punctul de intrare principal al programului. Primul lucru nou pe care îl facem cu modulul asyncio este să obținem bucla de evenimente. Bucla de evenimente se ocupă de tot codul asincron. Apoi, bucla este rulată până când se completează și trece funcția main
. Există o nouă sintaxă în definiția main: async def
. Veți observa, de asemenea, await
și with async
.
Sintaxa async/wait a fost introdusă în PEP492. Sintaxa async def
marchează o funcție ca o rutină. Pe plan intern, coroutinele se bazează pe generatoare Python, dar nu sunt exact același lucru. Coroutine returnează un obiect coroutine similar cu modul în care generatoarele returnează un obiect generator. Odată ce ai o corutină, îi obții rezultatele cu expresia await
. Când apelurile unei corutine await
, execuția corutinei este suspendată până la finalizarea awaitable-ului. Această suspendare permite finalizarea altor lucrări în timp ce corutina este suspendată „în așteptarea” unui rezultat. În general, acest rezultat va fi un fel de I/O, cum ar fi o cerere de bază de date sau, în cazul nostru, o solicitare HTTP.
Funcția download_link
a trebuit să fie schimbată destul de semnificativ. Anterior, ne bazam pe urllib
pentru a face cea mai mare parte a muncii de citire a imaginii pentru noi. Acum, pentru a permite metodei noastre să funcționeze corect cu paradigma de programare asincronă, am introdus o buclă while
care citește bucăți din imagine la un moment dat și suspendă execuția în timp ce așteptăm finalizarea I/O. Acest lucru permite buclei evenimentului să descărceze diferite imagini, deoarece fiecare are date noi disponibile în timpul descărcării.
Ar trebui să existe una — de preferință doar una — mod evident de a face asta
În timp ce zen-ul lui Python ne spune că ar trebui să existe o modalitate evidentă de a face ceva, există multe moduri în Python de a introduce concurența în programele noastre. Cea mai bună metodă de a alege va depinde de cazul dvs. de utilizare specific. Paradigma asincronă se adaptează mai bine la sarcinile de lucru cu concurență ridicată (cum ar fi un server web) în comparație cu threading sau multiprocesare, dar necesită ca codul (și dependențele) să fie asincron pentru a beneficia pe deplin.
Sperăm că exemplele de threading Python din acest articol — și actualizarea — vă vor îndrepta în direcția corectă, astfel încât să aveți o idee unde să căutați în biblioteca standard Python dacă trebuie să introduceți concurența în programele dvs.