Išsamus Python concurrent.futures modulio vadovas, lyginantis ThreadPoolExecutor ir ProcessPoolExecutor lygiagrečiam užduočių vykdymui, su praktiniais pavyzdžiais.
Lygiagretumo atrakinimas Python: ThreadPoolExecutor prieš ProcessPoolExecutor
Python, būdamas universali ir plačiai naudojama programavimo kalba, turi tam tikrų apribojimų, kalbant apie tikrą paralelumą dėl Globalaus Interpretatoriaus Užrakto (GIL). concurrent.futures
modulis suteikia aukšto lygio sąsają asinchroniškai vykdyti iškviečiamuosius objektus, siūlydamas būdą apeiti kai kuriuos iš šių apribojimų ir pagerinti našumą tam tikriems užduočių tipams. Šis modulis suteikia dvi pagrindines klases: ThreadPoolExecutor
ir ProcessPoolExecutor
. Šis išsamus vadovas išnagrinės abi, pabrėždamas jų skirtumus, stipriąsias ir silpnąsias puses ir pateikdamas praktinių pavyzdžių, kad padėtų jums pasirinkti tinkamą vykdytoją jūsų poreikiams.
Lygiagretumo ir Paralelumo Supratimas
Prieš gilinantis į kiekvieno vykdytojo specifiką, būtina suprasti lygiagretumo ir paralelumo sąvokas. Šie terminai dažnai naudojami pakaitomis, tačiau jie turi skirtingas reikšmes:
- Lygiagretumas: Susijęs su kelių užduočių valdymu tuo pačiu metu. Tai apie jūsų kodo struktūravimą, kad būtų galima vienu metu tvarkyti kelis dalykus, net jei jie iš tikrųjų yra įterpti viename procesoriaus branduolyje. Pagalvokite apie tai kaip apie virėją, kuris valdo kelis puodus ant vienos viryklės – jie visi nevirsta *tiksliai* tuo pačiu metu, bet virėjas juos visus valdo.
- Paralelumas: Apima faktinį kelių užduočių vykdymą *tuo pačiu* metu, paprastai naudojant kelis procesoriaus branduolius. Tai tarsi turėti kelis virėjus, kurių kiekvienas vienu metu dirba su skirtinga patiekalo dalimi.
Python GIL iš esmės neleidžia tikro paralelumo CPU-susietoms užduotims, kai naudojami gijos. Taip yra todėl, kad GIL leidžia tik vienai gijai valdyti Python interpretatorių bet kuriuo metu. Tačiau I/O-susietoms užduotims, kai programa didžiąją laiko dalį praleidžia laukdama išorinių operacijų, tokių kaip tinklo užklausos ar disko skaitymai, gijos vis tiek gali žymiai pagerinti našumą, leisdamos kitoms gijoms veikti, kol viena laukia.
Pristatome `concurrent.futures` Modulį
concurrent.futures
modulis supaprastina užduočių vykdymo asinchroniškai procesą. Jis suteikia aukšto lygio sąsają darbui su gijomis ir procesais, abstrahuodamas didžiąją dalį sudėtingumo, susijusio su tiesioginiu jų valdymu. Pagrindinė koncepcija yra "vykdytojas", kuris valdo pateiktų užduočių vykdymą. Du pagrindiniai vykdytojai yra:
ThreadPoolExecutor
: Naudoja gijų rinkinį užduotims vykdyti. Tinka I/O-susietoms užduotims.ProcessPoolExecutor
: Naudoja procesų rinkinį užduotims vykdyti. Tinka CPU-susietoms užduotims.
ThreadPoolExecutor: Gijų panaudojimas I/O-Susietoms Užduotims
ThreadPoolExecutor
sukuria darbuotojų gijų rinkinį užduotims vykdyti. Dėl GIL gijos nėra idealios skaičiavimo intensyvioms operacijoms, kurios gauna naudos iš tikro paralelumo. Tačiau jos puikiai tinka I/O-susietiems scenarijams. Pažiūrėkime, kaip ją naudoti:
Pagrindinis Naudojimas
Štai paprastas ThreadPoolExecutor
naudojimo pavyzdys, norint vienu metu atsisiųsti kelis tinklalapius:
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")
Paaiškinimas:
- Importuojame reikiamus modulius:
concurrent.futures
,requests
irtime
. - Apibrėžiame atsisiunčiamų URL adresų sąrašą.
download_page
funkcija atsiunčia duoto URL turinį. Klaidų apdorojimas įtrauktas naudojant `try...except` ir `response.raise_for_status()`, kad būtų galima aptikti galimas tinklo problemas.- Sukuriame
ThreadPoolExecutor
su daugiausia 4 darbuotojų gijomis. Argumentasmax_workers
valdo didžiausią gijų skaičių, kurį galima naudoti vienu metu. Jei nustatysite per aukštą, tai ne visada pagerins našumą, ypač I/O susietoms užduotims, kai tinklo pralaidumas dažnai yra kliūtis. - Naudojame sąrašo suvokimą, kad pateiktume kiekvieną URL vykdytojui naudodami
executor.submit(download_page, url)
. Tai grąžinaFuture
objektą kiekvienai užduočiai. concurrent.futures.as_completed(futures)
funkcija grąžina iteratorių, kuris generuoja ateities objektus jiems baigus. Tai leidžia išvengti laukimo, kol visos užduotys bus baigtos, prieš apdorojant rezultatus.- Iteruojame per užbaigtus ateities objektus ir gauname kiekvienos užduoties rezultatą naudodami
future.result()
, susumuodami bendrą atsisiųstų baitų skaičių. Klaidų apdorojimasdownload_page
užtikrina, kad atskiros klaidos nesugadintų viso proceso. - Galiausiai atspausdiname bendrą atsisiųstų baitų skaičių ir sugaištą laiką.
ThreadPoolExecutor Privalumai
- Supaprastintas Lygiagretumas: Suteikia švarią ir lengvai naudojamą sąsają gijoms valdyti.
- I/O-Susieto Našumas: Puikiai tinka užduotims, kurios didžiąją laiko dalį praleidžia laukdamos I/O operacijų, tokių kaip tinklo užklausos, failų skaitymai ar duomenų bazių užklausos.
- Sumažinta Viršutinė Dalis: Gijos paprastai turi mažesnę viršutinę dalį, palyginti su procesais, todėl jos yra efektyvesnės užduotims, kurios apima dažną konteksto perjungimą.
ThreadPoolExecutor Apribojimai
- GIL Apribojimas: GIL apriboja tikrą paralelumą CPU susietoms užduotims. Tik viena gija vienu metu gali vykdyti Python baitų kodą, paneigdama kelių branduolių naudą.
- Dėmesio Sudėtingumas: Daugiagijų programų derinimas gali būti sudėtingas dėl lenktynių sąlygų ir kitų su lygiagretumu susijusių problemų.
ProcessPoolExecutor: Daugiaprocesiškumo panaudojimas CPU-Susietoms Užduotims
ProcessPoolExecutor
įveikia GIL apribojimą, sukuriant darbuotojų procesų rinkinį. Kiekvienas procesas turi savo Python interpretatorių ir atminties erdvę, leidžiančią tikrą paralelumą daugiašerdėse sistemose. Tai idealiai tinka CPU susietoms užduotims, kurios apima sunkius skaičiavimus.
Pagrindinis Naudojimas
Apsvarstykite skaičiavimo intensyvią užduotį, pvz., kvadratų sumos apskaičiavimą dideliam skaičių diapazonui. Štai kaip naudoti ProcessPoolExecutor
, kad lygiagrečiai atliktumėte šią užduotį:
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")
Paaiškinimas:
- Apibrėžiame funkciją
sum_of_squares
, kuri apskaičiuoja kvadratų sumą duotam skaičių diapazonui. Įtraukiame `os.getpid()`, kad pamatytume, kuris procesas vykdo kiekvieną diapazoną. - Apibrėžiame diapazono dydį ir naudojamų procesų skaičių.
ranges
sąrašas sukurtas tam, kad padalintų bendrą skaičiavimo diapazoną į mažesnes dalis, po vieną kiekvienam procesui. - Sukuriame
ProcessPoolExecutor
su nurodytu darbuotojų procesų skaičiumi. - Pateikiame kiekvieną diapazoną vykdytojui naudodami
executor.submit(sum_of_squares, start, end)
. - Renkame kiekvieno ateities objekto rezultatus naudodami
future.result()
. - Susumuojame visų procesų rezultatus, kad gautume galutinę sumą.
Svarbi Pastaba: Naudojant ProcessPoolExecutor
, ypač Windows sistemoje, kodą, kuris sukuria vykdytoją, turėtumėte įdėti į if __name__ == "__main__":
bloką. Tai apsaugo nuo rekursyvaus proceso generavimo, kuris gali sukelti klaidų ir netikėto elgesio. Taip yra todėl, kad modulis yra iš naujo importuojamas kiekviename vaiko procese.
ProcessPoolExecutor Privalumai
- Tikras Paralelumas: Įveikia GIL apribojimą, leidžiantį tikrą paralelumą daugiašerdėse sistemose CPU susietoms užduotims.
- Pagerintas Našumas CPU-Susietoms Užduotims: Galima pasiekti reikšmingą našumo padidėjimą skaičiavimo intensyvioms operacijoms.
- Patikimumas: Jei vienas procesas sugenda, tai nebūtinai sugadina visą programą, nes procesai yra izoliuoti vienas nuo kito.
ProcessPoolExecutor Apribojimai
- Didesnė Viršutinė Dalis: Procesų kūrimas ir valdymas turi didesnę viršutinę dalį, palyginti su gijomis.
- Tarp-Procesinis Ryšys: Dalijimasis duomenimis tarp procesų gali būti sudėtingesnis ir reikalauja tarp-procesinio ryšio (IPC) mechanizmų, kurie gali padidinti viršutinę dalį.
- Atminties Pėdsakas: Kiekvienas procesas turi savo atminties erdvę, kuri gali padidinti bendrą programos atminties pėdsaką. Didelių duomenų kiekių perdavimas tarp procesų gali tapti kliūtimi.
Tinkamo Vykdytojo Pasirinkimas: ThreadPoolExecutor prieš ProcessPoolExecutor
Raktas į pasirinkimą tarpThreadPoolExecutor
ir ProcessPoolExecutor
slypi suprantant jūsų užduočių pobūdį:
- I/O-Susietos Užduotys: Jei jūsų užduotys didžiąją laiko dalį praleidžia laukdamos I/O operacijų (pvz., tinklo užklausos, failų skaitymai, duomenų bazių užklausos),
ThreadPoolExecutor
paprastai yra geresnis pasirinkimas. GIL šiose situacijose yra mažiau kliūtis, o mažesnė gijų viršutinė dalis daro jas efektyvesnes. - CPU-Susietos Užduotys: Jei jūsų užduotys yra skaičiavimo intensyvios ir naudoja kelis branduolius,
ProcessPoolExecutor
yra tinkamas pasirinkimas. Jis apeina GIL apribojimą ir leidžia tikrą paralelumą, todėl žymiai pagerėja našumas.
Štai lentelė, apibendrinanti pagrindinius skirtumus:
Funkcija | ThreadPoolExecutor | ProcessPoolExecutor |
---|---|---|
Lygiagretumo Modelis | Daugiagijumas | Daugiaprocesiškumas |
GIL Poveikis | Ribojamas GIL | Apeina GIL |
Tinka | I/O-susietoms užduotims | CPU-susietoms užduotims |
Viršutinė Dalis | Mažesnė | Didesnė |
Atminties Pėdsakas | Mažesnis | Didesnis |
Tarp-Procesinis Ryšys | Nebūtinas (gijos dalijasi atmintimi) | Reikalingas duomenims dalytis |
Patikimumas | Mažiau patikimas (gedimas gali paveikti visą procesą) | Patikimesnis (procesai yra izoliuoti) |
Išplėstinės Technikos ir Svarstymai
Užduočių Pateikimas su Argumentais
Abu vykdytojai leidžia perduoti argumentus vykdomai funkcijai. Tai daroma naudojant submit()
metodą:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(my_function, arg1, arg2)
result = future.result()
Klaidų Apdorojimas
Klaidos, iškeltos vykdomoje funkcijoje, nėra automatiškai perduodamos pagrindinei gijai ar procesui. Turite aiškiai jas apdoroti, kai gaunate Future
rezultatą:
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` Naudojimas Paprastoms Užduotims
Paprastoms užduotims, kai norite pritaikyti tą pačią funkciją įvesties sekai, map()
metodas suteikia glaustą būdą pateikti užduotis:
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))
Darbuotojų Skaičiaus Valdymas
Argumentas max_workers
tiek ThreadPoolExecutor
, tiek ProcessPoolExecutor
valdo didžiausią gijų ar procesų skaičių, kurį galima naudoti vienu metu. Tinkamos max_workers
reikšmės pasirinkimas yra svarbus našumui. Gera atspirties taškas yra procesoriaus branduolių skaičius jūsų sistemoje. Tačiau I/O susietoms užduotims galite gauti naudos naudodami daugiau gijų nei branduolių, nes gijos gali perjungti kitas užduotis, laukdamos I/O. Dažnai reikia eksperimentuoti ir profiliuoti, kad nustatytumėte optimalią vertę.
Pažangos Stebėjimas
concurrent.futures
modulis neteikia įmontuotų mechanizmų tiesiogiai stebėti užduočių eigą. Tačiau galite įdiegti savo pažangos stebėjimą naudodami atgalinius skambučius arba bendrus kintamuosius. Bibliotekos, tokios kaip `tqdm`, gali būti integruotos, kad būtų rodomos pažangos juostos.
Realūs Pavyzdžiai
Apsvarstykime keletą realių scenarijų, kai ThreadPoolExecutor
ir ProcessPoolExecutor
gali būti veiksmingai pritaikyti:
- Tinklalapių Skrepavimas: Atsisiųsti ir analizuoti kelis tinklalapius vienu metu naudojant
ThreadPoolExecutor
. Kiekviena gija gali tvarkyti skirtingą tinklalapį, pagerindama bendrą skrepavimo greitį. Nepamirškite svetainės paslaugų teikimo sąlygų ir venkite perkrauti jų serverius. - Vaizdo Apdorojimas: Pritaikyti vaizdo filtrus arba transformacijas dideliam vaizdų rinkiniui naudojant
ProcessPoolExecutor
. Kiekvienas procesas gali tvarkyti skirtingą vaizdą, panaudodamas kelis branduolius greitesniam apdorojimui. Apsvarstykite bibliotekas, tokias kaip OpenCV, efektyviam vaizdo manipuliavimui. - Duomenų Analizė: Atlikti sudėtingus skaičiavimus su dideliais duomenų rinkiniais naudojant
ProcessPoolExecutor
. Kiekvienas procesas gali analizuoti duomenų pogrupį, sumažindamas bendrą analizės laiką. Pandas ir NumPy yra populiarios bibliotekos duomenų analizei Python. - Mašininis Mokymasis: Mokyti mašininio mokymosi modelius naudojant
ProcessPoolExecutor
. Kai kuriuos mašininio mokymosi algoritmus galima efektyviai lygiagrečiai atlikti, leidžiant greičiau mokyti. Bibliotekos, tokios kaip scikit-learn ir TensorFlow, siūlo palaikymą lygiagretumui. - Vaizdo Kodavimas: Konvertuoti vaizdo failus į skirtingus formatus naudojant
ProcessPoolExecutor
. Kiekvienas procesas gali koduoti skirtingą vaizdo segmentą, todėl bendras kodavimo procesas yra greitesnis.
Globalūs Aspektai
Kuriant lygiagrečias programas pasaulinei auditorijai, svarbu atsižvelgti į šiuos dalykus:
- Laiko Zonos: Nepamirškite laiko zonų, kai atliekate laikui jautrias operacijas. Naudokite bibliotekas, tokias kaip
pytz
, laiko zonų konvertavimui. - Lokalės: Užtikrinkite, kad jūsų programa tinkamai tvarkytų skirtingas lokalės. Naudokite bibliotekas, tokias kaip
locale
, kad formatuotumėte skaičius, datas ir valiutas pagal vartotojo lokalę. - Simbolių Kodavimas: Naudokite Unicode (UTF-8) kaip numatytąjį simbolių kodavimą, kad palaikytumėte platų kalbų spektrą.
- Internacionalizacija (i18n) ir Lokalizacija (l10n): Sukurkite savo programą taip, kad ją būtų galima lengvai internacionalizuoti ir lokalizuoti. Naudokite gettext arba kitas vertimo bibliotekas, kad pateiktumėte skirtingų kalbų vertimus.
- Tinklo Latencija: Apsvarstykite tinklo latenciją, kai bendraujate su nuotolinėmis paslaugomis. Įdiekite atitinkamus skirtuosius laikus ir klaidų apdorojimą, kad užtikrintumėte, jog jūsų programa yra atspari tinklo problemoms. Geografinė serverių vieta gali labai paveikti latenciją. Apsvarstykite galimybę naudoti turinio pristatymo tinklus (CDN), kad pagerintumėte našumą vartotojams skirtinguose regionuose.
Išvada
concurrent.futures
modulis suteikia galingą ir patogų būdą įvesti lygiagretumą ir paralelumą į jūsų Python programas. Suprasdami skirtumus tarp ThreadPoolExecutor
ir ProcessPoolExecutor
ir atidžiai apsvarstydami savo užduočių pobūdį, galite žymiai pagerinti savo kodo našumą ir reakciją. Nepamirškite profiliuoti savo kodo ir eksperimentuoti su skirtingomis konfigūracijomis, kad rastumėte optimalius nustatymus konkrečiam naudojimo atvejui. Taip pat žinokite apie GIL apribojimus ir galimus daugiagijų ir daugiaprocesių programavimo sudėtingumus. Kruopščiai planuodami ir įgyvendindami, galite atskleisti visą lygiagretumo potencialą Python ir sukurti patikimas ir keičiamo dydžio programas pasaulinei auditorijai.