Pythoni `concurrent.futures` mooduli juhend, võrreldes ThreadPoolExecutori ja ProcessPoolExecutori paralleelseks tööks, praktiliste näidetega.
Samaaegsuse vabastamine Pythonis: ThreadPoolExecutor vs. ProcessPoolExecutor
Python, kuigi mitmekülgne ja laialdaselt kasutatav programmeerimiskeel, omab teatud piiranguid tõelise paralleelsuse osas tänu globaalsele interpretaatori lukule (GIL). Moodul concurrent.futures
pakub kõrgetasemelist liidest käivitatavate objektide asünkroonseks täitmiseks, pakkudes võimalust neist piirangutest mööda minna ja parandada jõudlust teatud tüüpi ülesannete puhul. See moodul pakub kahte põhiklassi: ThreadPoolExecutor
ja ProcessPoolExecutor
. See põhjalik juhend uurib mõlemat, rõhutades nende erinevusi, tugevusi ja nõrkusi ning pakkudes praktilisi näiteid, mis aitavad teil valida oma vajadustele sobiva täituri.
Samaaegsuse ja paralleelsuse mõistmine
Enne iga täituri eripäradesse süvenemist on oluline mõista samaaegsuse ja paralleelsuse mõisteid. Neid termineid kasutatakse sageli sünonüümidena, kuid neil on selged tähendused:
- Samaaegsus (Concurrency): Tegeleb mitme ülesande haldamisega samal ajal. See on koodi struktureerimine nii, et see käitleks mitut asja näiliselt samaaegselt, isegi kui need on tegelikult ühe protsessorituuma peal põimitud. Mõelge sellele kui kokale, kes haldab mitut potti ühel pliidil – need kõik ei kee *täpselt* samal hetkel, kuid kokk haldab neid kõiki.
- Paralleelsus (Parallelism): Hõlmab tegelikult mitme ülesande täitmist *samal* ajal, tavaliselt mitme protsessorituuma kasutamise kaudu. See on nagu mitme koka omamine, kellest igaüks töötab samaaegselt söögi eri osaga.
Pythoni GIL takistab suuresti tõelist paralleelsust CPU-piiratud ülesannete puhul, kui kasutada lõimesid. See on tingitud sellest, et GIL lubab korraga ainult ühel lõimel hoida Pythoni interpretaatori kontrolli. I/O-piiratud ülesannete puhul aga, kus programm veedab suurema osa oma ajast oodates väliseid operatsioone, nagu võrgupäringud või kettalt lugemine, võivad lõimed siiski pakkuda olulisi jõudluse paranemisi, lubades teistel lõimedel joosta, samal ajal kui üks ootab.
Tutvustame moodulit concurrent.futures
Moodul concurrent.futures
lihtsustab ülesannete asünkroonse täitmise protsessi. See pakub kõrgetasemelist liidest lõimede ja protsessidega töötamiseks, abstraheerides suure osa nende otsese haldamisega kaasnevast keerukusest. Põhimõiste on "täitur" (executor), mis haldab esitatud ülesannete täitmist. Kaks peamist täiturit on:
ThreadPoolExecutor
: Kasutab lõimede kogumit ülesannete täitmiseks. Sobib I/O-piiratud ülesannete jaoks.ProcessPoolExecutor
: Kasutab protsesside kogumit ülesannete täitmiseks. Sobib CPU-piiratud ülesannete jaoks.
ThreadPoolExecutor: Lõimede kasutamine I/O-piiratud ülesannete jaoks
ThreadPoolExecutor
loob töötluslõimede kogumi ülesannete täitmiseks. GIL-i tõttu ei sobi lõimed ideaalselt arvutusmahukate operatsioonide jaoks, mis saavad kasu tõelisest paralleelsusest. Siiski on need I/O-piiratud stsenaariumides suurepärased. Uurime, kuidas seda kasutada:
Põhikasutus
Siin on lihtne näide ThreadPoolExecutor
-i kasutamisest mitme veebilehe samaaegseks allalaadimiseks:
import concurrent.futures
import requests
import time
urls = [
"https://www.example.com",
"https://www.google.com",
"https://www.wikipedia.org",
"https://www.python.org"
]
def download_page(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
print(f"Downloaded {url}: {len(response.content)} bytes")
return len(response.content)
except requests.exceptions.RequestException as e:
print(f"Error downloading {url}: {e}")
return 0
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# Submit each URL to the executor
futures = [executor.submit(download_page, url) for url in urls]
# Wait for all tasks to complete
total_bytes = sum(future.result() for future in concurrent.futures.as_completed(futures))
print(f"Total bytes downloaded: {total_bytes}")
print(f"Time taken: {time.time() - start_time:.2f} seconds")
Selgitus:
- Impordime vajalikud moodulid:
concurrent.futures
,requests
jatime
. - Määratleme allalaadimiseks URL-ide loendi.
- Funktsioon
download_page
hangib antud URL-i sisu. Veakäsitlus on lisatud, kasutades `try...except` ja `response.raise_for_status()`, et püüda kinni võimalikud võrguprobleemid. - Loome
ThreadPoolExecutor
-i maksimaalselt 4 töötluslõimega. Argumentmax_workers
kontrollib samaaegselt kasutatavate lõimede maksimaalset arvu. Liiga kõrgeks seadmine ei pruugi alati jõudlust parandada, eriti I/O-piiratud ülesannete puhul, kus võrgu ribalaius on sageli kitsaskohaks. - Kasutame loendiarusaama, et esitada iga URL täiturile, kasutades
executor.submit(download_page, url)
. See tagastab iga ülesande jaoksFuture
objekti. - Funktsioon
concurrent.futures.as_completed(futures)
tagastab iteraatori, mis annab tulemusi nii, nagu need valmivad. See väldib ootamist, kuni kõik ülesanded on enne tulemuste töötlemist lõpule viidud. - Itereerime lõpetatud tulemuste üle ja hangime iga ülesande tulemuse, kasutades
future.result()
, summeerides allalaaditud baitide koguarvu. Veakäsitlus funktsioonis `download_page` tagab, et üksikud vead ei vii kogu protsessi kokku. - Lõpuks prindime allalaaditud baitide koguarvu ja kulunud aja.
ThreadPoolExecutori eelised
- Lihtsustatud samaaegsus: Pakub puhta ja hõlpsasti kasutatava liidese lõimede haldamiseks.
- I/O-piiratud jõudlus: Suurepärane ülesannete jaoks, mis kulutavad märkimisväärse osa ajast I/O-operatsioonide ootamisele, näiteks võrgupäringud, faililugemised või andmebaasipäringud.
- Vähendatud üldkulud: Lõimedel on üldjuhul protsessidega võrreldes madalamad üldkulud, mis muudab need tõhusamaks ülesannete puhul, mis hõlmavad sagedast konteksti vahetust.
ThreadPoolExecutori piirangud
- GIL-i piirang: GIL piirab tõelist paralleelsust CPU-piiratud ülesannete puhul. Korraga saab Pythoni baitkoodi täita ainult üks lõim, mis nullib mitmetuumaliste protsessorite eelised.
- Silumise keerukus: Mitmelõimeliste rakenduste silumine võib olla keeruline võidusõidutingimuste ja muude samaaegsusega seotud probleemide tõttu.
ProcessPoolExecutor: Mitme protsessi vabastamine CPU-piiratud ülesannete jaoks
ProcessPoolExecutor
ületab GIL-i piirangu, luues töötlusprotsesside kogumi. Igal protsessil on oma Pythoni interpretaator ja mäluruum, mis võimaldab tõelist paralleelsust mitmetuumalistes süsteemides. See muudab selle ideaalseks CPU-piiratud ülesannete jaoks, mis hõlmavad suuri arvutusi.
Põhikasutus
Mõelgem arvutusmahukale ülesandele, näiteks ruutude summa arvutamisele suure arvude vahemiku jaoks. Siin on, kuidas kasutada ProcessPoolExecutor
-it selle ülesande paralleelseks täitmiseks:
import concurrent.futures
import time
import os
def sum_of_squares(start, end):
pid = os.getpid()
print(f"Process ID: {pid}, Calculating sum of squares from {start} to {end}")
total = 0
for i in range(start, end + 1):
total += i * i
return total
if __name__ == "__main__": #Important for avoiding recursive spawning in some environments
start_time = time.time()
range_size = 1000000
num_processes = 4
ranges = [(i * range_size + 1, (i + 1) * range_size) for i in range(num_processes)]
with concurrent.futures.ProcessPoolExecutor(max_workers=num_processes) as executor:
futures = [executor.submit(sum_of_squares, start, end) for start, end in ranges]
results = [future.result() for future in concurrent.futures.as_completed(futures)]
total_sum = sum(results)
print(f"Total sum of squares: {total_sum}")
print(f"Time taken: {time.time() - start_time:.2f} seconds")
Selgitus:
- Määratleme funktsiooni
sum_of_squares
, mis arvutab antud arvude vahemiku ruutude summa. Lisame `os.getpid()`, et näha, milline protsess iga vahemikku täidab. - Määratleme vahemiku suuruse ja kasutatavate protsesside arvu. Loend
ranges
luuakse kogu arvutusvahemiku jagamiseks väiksemateks osadeks, üks iga protsessi jaoks. - Loome
ProcessPoolExecutor
-i määratud arvu töötlusprotsessidega. - Esitame iga vahemiku täiturile, kasutades
executor.submit(sum_of_squares, start, end)
. - Kogume iga tulemuse tulemuse, kasutades
future.result()
. - Summeerime kõigi protsesside tulemused, et saada lõplik summa.
Oluline märkus: ProcessPoolExecutor
-i kasutamisel, eriti Windowsis, peaksite täituri loova koodi paigutama if __name__ == "__main__":
ploki sisse. See hoiab ära rekursiivse protsesside loomise, mis võib põhjustada vigu ja ootamatut käitumist. See on tingitud sellest, et moodul imporditakse igas alamprotsessis uuesti.
ProcessPoolExecutori eelised
- Tõeline paralleelsus: Ületab GIL-i piirangu, võimaldades CPU-piiratud ülesannete puhul tõelist paralleelsust mitmetuumalistes süsteemides.
- Parem jõudlus CPU-piiratud ülesannete jaoks: Arvutusmahukate operatsioonide puhul on võimalik saavutada märkimisväärset jõudluse kasvu.
- Töökindlus: Kui üks protsess katkeb, ei too see tingimata alla kogu programmi, kuna protsessid on üksteisest isoleeritud.
ProcessPoolExecutori piirangud
- Kõrgemad üldkulud: Protsesside loomine ja haldamine on lõimedega võrreldes kallim.
- Protsessidevaheline side: Andmete jagamine protsesside vahel võib olla keerulisem ja nõuab protsessidevahelise side (IPC) mehhanisme, mis võivad lisada üldkulusid.
- Mälujälg: Igal protsessil on oma mäluruum, mis võib suurendada rakenduse üldist mälujälge. Suurte andmehulkade edastamine protsesside vahel võib muutuda kitsaskohaks.
Õige täituri valimine: ThreadPoolExecutor vs. ProcessPoolExecutor
Võti ThreadPoolExecutor
ja ProcessPoolExecutor
vahel valimisel seisneb teie ülesannete olemuse mõistmises:
- I/O-piiratud ülesanded: Kui teie ülesanded veedavad suurema osa ajast I/O-operatsioonide ootamisel (nt võrgupäringud, faililugemised, andmebaasipäringud), on
ThreadPoolExecutor
üldiselt parem valik. GIL on nendes stsenaariumides väiksem kitsaskoht ja lõimede madalamad üldkulud muudavad need tõhusamaks. - CPU-piiratud ülesanded: Kui teie ülesanded on arvutusmahukad ja kasutavad mitut tuuma, on
ProcessPoolExecutor
õige tee. See möödub GIL-i piirangust ja võimaldab tõelist paralleelsust, mille tulemuseks on märkimisväärsed jõudluse paranemised.
Siin on tabel, mis võtab kokku peamised erinevused:
Funktsioon | ThreadPoolExecutor | ProcessPoolExecutor |
---|---|---|
Samaaegsuse mudel | Mitmelõimeline | Mitmeprotsessiline |
GIL-i mõju | Piiratud GIL-iga | Möödub GIL-ist |
Sobib | I/O-piiratud ülesanded | CPU-piiratud ülesanded |
Üldkulud | Madalam | Kõrgem |
Mälujälg | Madalam | Kõrgem |
Protsessidevaheline side | Pole nõutav (lõimed jagavad mälu) | Nõutav andmete jagamiseks |
Töökindlus | Vähem töökindel (kokkujooksmine võib mõjutada kogu protsessi) | Töökindlam (protsessid on isoleeritud) |
Täpsemad tehnikad ja kaalutlused
Ülesannete esitamine argumentidega
Mõlemad täiturid võimaldavad teil edastada argumente täidetavale funktsioonile. See toimub meetodi submit()
kaudu:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(my_function, arg1, arg2)
result = future.result()
Erandite käsitlemine
Täidetud funktsioonis tekkinud erandid ei levi automaatselt pealõimele ega -protsessile. Peate neid selgesõnaliselt käsitlema, kui hangite Future
tulemuse:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(my_function)
try:
result = future.result()
except Exception as e:
print(f"An exception occurred: {e}")
map
-i kasutamine lihtsate ülesannete jaoks
Lihtsate ülesannete jaoks, kus soovite rakendada sama funktsiooni sisendite järjestikusele hulgale, pakub meetod map()
lühikest viisi ülesannete esitamiseks:
def square(x):
return x * x
with concurrent.futures.ProcessPoolExecutor() as executor:
numbers = [1, 2, 3, 4, 5]
results = executor.map(square, numbers)
print(list(results))
Töötajate arvu kontrollimine
Argument max_workers
nii ThreadPoolExecutor
-is kui ka ProcessPoolExecutor
-is kontrollib samaaegselt kasutatavate lõimede või protsesside maksimaalset arvu. Õige väärtuse valimine max_workers
jaoks on jõudluse seisukohalt oluline. Hea lähtepunkt on teie süsteemis saadaolevate CPU tuumade arv. Kuid I/O-piiratud ülesannete puhul võite saada kasu suurema arvu lõimede kasutamisest kui tuumasid, kuna lõimed saavad I/O ootamise ajal teistele ülesannetele üle minna. Optimaalse väärtuse määramiseks on sageli vaja eksperimenteerimist ja profileerimist.
Edusammude jälgimine
Moodul concurrent.futures
ei paku sisseehitatud mehhanisme ülesannete edenemise otseseks jälgimiseks. Kuid saate rakendada oma edenemise jälgimist, kasutades tagasihelistamisi (callbacks) või jagatud muutujaid. Teeki `tqdm` saab integreerida edenemisribade kuvamiseks.
Pärismaailma näited
Vaatame mõningaid pärismaailma stsenaariume, kus ThreadPoolExecutor
ja ProcessPoolExecutor
-it saab tõhusalt rakendada:
- Veebikaapimine: Mitme veebilehe allalaadimine ja parsimine samaaegselt, kasutades
ThreadPoolExecutor
-it. Iga lõim saab käsitleda erinevat veebilehte, parandades üldist kaapimiskiirust. Olge teadlik veebilehe teenusetingimustest ja vältige nende serverite ülekoormamist. - Pilditöötlus: Pildifiltrite või transformatsioonide rakendamine suurele piltide hulgale, kasutades
ProcessPoolExecutor
-it. Iga protsess saab käsitleda erinevat pilti, kasutades mitut tuuma kiiremaks töötlemiseks. Tõhusaks pilditöötluseks kaaluge teeke nagu OpenCV. - Andmeanalüüs: Keeruliste arvutuste teostamine suurte andmekogumite peal, kasutades
ProcessPoolExecutor
-it. Iga protsess saab analüüsida andmete alamhulka, vähendades üldist analüüsimisaega. Pandas ja NumPy on Pythonis andmeanalüüsiks populaarsed teegid. - Masinõpe: Masinõppemudelite treenimine, kasutades
ProcessPoolExecutor
-it. Mõned masinõppe algoritmid on tõhusalt paralleelitavad, võimaldades kiiremaid treenimisaegu. Teegid nagu scikit-learn ja TensorFlow pakuvad paralleelsuse tuge. - Video kodeerimine: Videofailide teisendamine erinevatesse formaatidesse, kasutades
ProcessPoolExecutor
-it. Iga protsess saab kodeerida erinevat videosegmenti, muutes üldise kodeerimisprotsessi kiiremaks.
Globaalsed kaalutlused
Samaaegsete rakenduste arendamisel globaalsele auditooriumile on oluline arvestada järgmiste teguritega:
- Ajavööndid: Olge ajavöönditega arvestamisel ajatundlike toimingutega. Kasutage ajavööndite teisenduste käsitlemiseks teeke nagu
pytz
. - Lokaadid: Veenduge, et teie rakendus käsitleks erinevaid lokaate õigesti. Kasutage teeke nagu
locale
numbrite, kuupäevade ja valuutade vormindamiseks vastavalt kasutaja lokaadile. - Märgistikud: Kasutage Unicode'i (UTF-8) vaikimisi märgistikuna, et toetada laia valikut keeli.
- Rahvusvahelistamine (i18n) ja lokaliseerimine (l10n): Kujundage oma rakendus nii, et seda oleks lihtne rahvusvahelistada ja lokaliseerida. Kasutage gettexti või muid tõlketeeke, et pakkuda tõlkeid erinevate keelte jaoks.
- Võrgu viivitus: Võtke arvesse võrgu viivitust kaugtjenustega suhtlemisel. Rakendage sobivad ajakatkestused ja veakäsitlus, et tagada teie rakenduse vastupidavus võrguprobleemidele. Serverite geograafiline asukoht võib viivitust märkimisväärselt mõjutada. Kaaluge sisu edastamise võrkude (CDN-ide) kasutamist, et parandada jõudlust kasutajatele erinevates piirkondades.
Kokkuvõte
Moodul concurrent.futures
pakub võimsa ja mugava viisi samaaegsuse ja paralleelsuse lisamiseks teie Pythoni rakendustesse. Mõistes erinevusi ThreadPoolExecutor
ja ProcessPoolExecutor
vahel ning kaaludes hoolikalt oma ülesannete olemust, saate oluliselt parandada oma koodi jõudlust ja reageerimisvõimet. Ärge unustage oma koodi profileerida ja katsetada erinevate konfiguratsioonidega, et leida oma konkreetse kasutusjuhtumi jaoks optimaalsed seaded. Olge ka teadlik GIL-i piirangutest ja mitmelõimelise ning mitmeprotsessilise programmeerimise võimalikest keerukustest. Hoolika planeerimise ja juurutamisega saate avada Pythoni samaaegsuse täieliku potentsiaali ning luua robustsed ja skaleeritavad rakendused globaalsele auditooriumile.