Inleiding tot parallelle en gelyktydige programmering in Python
Python is een van die gewildste tale vir dataverwerking en data-wetenskap in die algemeen. Die ekosisteem bied baie biblioteke en raamwerke wat hoëprestasie-rekenaars fasiliteer. Om parallelprogrammering in Python te doen, kan egter nogal moeilik wees.
In hierdie handleiding gaan ons bestudeer waarom parallelisme hard is, veral in die Python-konteks, en daarom gaan ons deur die volgende:
Hoekom is parallelisme lastig in Python (wenk: dit is as gevolg van die GIL-die globale tolk slot).
Drade versus prosesse: verskillende maniere om parallelisme te bereik. Wanneer om een ​​oor die ander te gebruik?
Parallel vs Gelyktydig: Waarom kan ons in sommige gevalle vir gelyktydigheid eerder as parallelisme vestig.
Bou 'n eenvoudige maar praktiese voorbeeld met behulp van die verskillende tegnieke wat bespreek word.
Die Global Interpreter Lock (GIL) is een van die mees omstrede vakke in die Python-wêreld. In CPython, die gewildste implementering van Python, is die GIL 'n mutex wat dinge veilig maak. Die GIL maak dit maklik om te integreer met eksterne biblioteke wat nie draadloos is nie, en dit maak nie-parallelle kode vinniger. Dit kom egter teen 'n koste. As gevolg van die GIL, kan ons nie werklike parallellisme bereik deur multithreading nie. Basies kan twee verskillende inheemse drade van dieselfde proses nie Python-kode gelyktydig uitvoer nie.
Dinge is egter nie so sleg nie, en hier is hoekom: dinge wat buite die GIL-ryk plaasvind, is vry om parallel te wees. In hierdie kategorie val langlopende take soos I / O en gelukkig is biblioteke soos numpies.
Drade versus Prosesse
So Python is nie werklik multithreaded. Maar wat is 'n draad? Kom ons neem 'n stap terug en kyk na dinge in perspektief.
'N proses is 'n basiese bedryfstelsel abstraksie. Dit is 'n program wat uitgevoer word - met ander woorde, kode wat aan die gang is. Meervoudige prosesse loop altyd in 'n rekenaar, en hulle word parallel uitgevoer.
'N Proses kan verskeie drade hê. Hulle voer dieselfde kode uit die ouerproses. Ideaal gesproke loop hulle parallel, maar nie noodwendig nie. Die rede waarom prosesse nie genoeg is nie, is dat aansoeke moet reageer en luister na gebruikersaksies terwyl die vertoning opgedateer word en 'n lêer gestoor word.
As dit nog 'n bietjie onduidelik is, is dit 'n cheatsheet:
Prosesse deel nie geheue nie
Drade deel geheue
Spawagings- / skakelprosesse is duur
Giet- / skakeldrade is goedkoper
Prosesse vereis meer hulpbronne
Gare benodig minder hulpbronne (word soms liggewig prosesse genoem)
Geen geheue-sinkronisasie benodig nie
U moet sinkronisasie meganismes gebruik om seker te wees dat u die data korrek hanteer
Daar is nie een resep wat alles akkommodeer nie. Die keuse van een is baie afhanklik van die konteks en die taak wat u probeer bereik.
Parallel vs Gelyktydig
Nou gaan ons een stap verder en duik in gelyktydigheid. Gelyktydigheid word dikwels misverstaan ​​en verkeerd vir parallelisme. Dit is nie die geval nie. Gelyktydigheid impliseer skedulering van onafhanklike kode wat op koöperatiewe wyse uitgevoer moet word. Profiteer van die feit dat 'n kode kode op I / O-bedrywighede wag en gedurende daardie tyd 'n ander, maar onafhanklike deel van die kode voer.
In Python, kan ons liggewig-gelyktydige gedrag bereik via groenletsels. Uit 'n paralleliseringsperspektief is die gebruik van drade of groentjies gelykwaardig omdat nie een van hulle parallel loop nie. Greenlets is selfs minder duur om te skep as drade. As gevolg hiervan word groenletsels sterk gebruik vir die uitvoer van 'n groot aantal eenvoudige I / O take, soos dié wat gewoonlik in netwerk- en webbedieners voorkom.
Noudat ons die verskil tussen drade en prosesse ken, parallel en gelyktydig, kan ons illustreer hoe verskillende take op die twee paradigmas uitgevoer word. Hier is wat ons gaan doen: ons sal verskeie kere 'n taak buite die GIL en een binne dit doen. Ons hardloop hulle seriaal, gebruik drade en gebruik prosesse. Kom ons definieer die take:
Ons het twee take geskep. Albei van hulle is langlopend, maar net crunch_numbers doen aktief berekeninge. Kom ons loop slegs serwieel, multithreaded en gebruik verskeie prosesse en vergelyk die resultate:
Hier is die uitset wat ek het (joune moet soortgelyk wees, alhoewel PID's en tye 'n bietjie verskil):
Hier is enkele waarnemings:
In die geval van die seriële benadering is dinge redelik voor die hand liggend. Ons doen die take een na die ander. Al vier lopies word uitgevoer deur dieselfde draad van dieselfde proses.
Met behulp van prosesse sny ons die uitvoeringstyd tot 'n kwart van die oorspronklike tyd, bloot omdat die take in parallel uitgevoer word. Let op hoe elke taak in 'n ander proses en op die hoofstuk van daardie proses uitgevoer word.
Met behulp van drade maak ons ​​voordeel uit die feit dat die take gelyktydig uitgevoer kan word. Die uitvoeringstyd word ook tot 'n kwart verminder, alhoewel niks parallel loop nie. : So gaan dit: ons gooi die eerste draad en dit begin wag vir die timer om te verstryk. Ons breek die uitvoering daarvan uit, laat dit wag vir die timer om te verstryk, en in hierdie tyd het ons die tweede draad gejaag. Ons herhaal dit vir al die drade. Op een oomblik verval die timer van die eerste draad, sodat ons die uitvoering daarvan verander en ons beëindig dit. Die algoritme word vir die tweede herhaal en vir al die ander drade. Aan die einde is die resultaat asof die dinge parallel loop. Jy sal ook sien dat die vier verskillende drade vertak uit en binne dieselfde proses woon: MainProcess.
Jy kan selfs sien dat die draad-benadering vinniger is as die werklike parallelle een. Dit is te wyte aan die oorhoofse spytprosesse. Soos ons vroeër opgemerk het, is spawning- en skakelprosesse 'n duur operasie.
Kom ons doen dieselfde roetine, maar hierdie keer hardloop die crunch_numbers taak
Hier is die uitset wat ek het:
Die belangrikste verskil hier is in die resultaat van die multithreaded benadering. Hierdie keer is dit baie soortgelyk aan die seriële benadering, en hier is hoekom: aangesien dit berekenings uitvoer en Python nie eintlike parallelisme verrig nie, loop die drade eintlik een na die ander, en gee uitvoering aan mekaar totdat hulle almal klaar is.
Die Python Parallelle / Gelyktydige Programmeringsekosisteem
Python het ryk API's vir parallelle / gelyktydige programmering. In hierdie handleiding dek ons ​​die gewildste, maar jy moet weet dat vir enige behoefte wat jy in hierdie domein het, daar waarskynlik iets daar buite is wat jou kan help om jou doel te bereik.
In die volgende afdeling bou ons 'n praktiese toepassing in baie vorme, met al die biblioteke wat aangebied word. Sonder verder, hier is die modules / biblioteke wat ons gaan dek:
threading: Die standaard manier van werk met drade in Python. Dit is 'n API-wrapper op hoër vlak oor die funksionaliteit wat deur die _thread-module blootgestel word, wat 'n lae-vlak koppelvlak oor die implementering van die bedryfstelsel is.
samelewingsfoute: 'n Module-deel van die standaardbiblioteek wat 'n selfs hoër vlak-abstraksielaag oor drade bied. Die drade word gemodelleer as asynchrone take.
Multiprocessing: Soortgelyk aan die threading module, wat 'n baie soortgelyke koppelvlak bied, maar prosesse in plaas van drade gebruik.
Gevangenes en Greenlets: Greenlets, ook bekend as mikrodrade, is eenhede van uitvoering wat gesamentlik geskeduleer kan word en kan take gelyktydig sonder veel oorhoofse werk verrig.
seldery: 'n Uitgegee taakwachtrij op hoë vlak. Die take word gekies en uitgevoer gelyktydig met behulp van verskeie paradigmas soos multiprocessing of gevent
Bou 'n praktiese toepassing
Om die teorie te ken, is mooi en goed, maar die beste manier om te leer is om iets prakties te bou, reg? In hierdie afdeling gaan ons 'n klassieke tipe toepassing bou wat deur al die verskillende paradigmas gaan.
Kom ons bou 'n program wat die uptime van webwerwe kontroleer. Daar is baie sulke oplossings daar buite, waarvan die bekendste waarskynlik Jetpack Monitor en Uptime Robot is. Die doel van hierdie programme is om u in kennis te stel wanneer u webwerf af is sodat u vinnig kan optree. Hier is hoe hulle werk:
Die aansoek gaan baie gereeld oor 'n lys van webwerf-URL's en kontroleer of die webwerwe op is.
Elke webwerf moet elke 5-10 minute nagegaan word sodat die stilstand nie beduidend is nie.
In plaas daarvan om 'n klassieke HTTP GET-versoek uit te voer, voer dit 'n HOOF-versoek uit, sodat dit nie jou verkeer aansienlik beïnvloed nie.
As die HTTP-status in die gevaarreekse (400+, 500+) is, word die eienaar in kennis gestel.
Die eienaar word per e-pos, sms-boodskap of push kennisgewing in kennis gestel.
Daarom is dit noodsaaklik om 'n parallel / gelyktydige benadering tot die probleem te neem. Namate die lys van webwerwe groei, sal ons nie elke vyf minute na die webblad gaan kyk nie. Die webwerwe kan ure af wees en die eienaar sal nie in kennis gestel word nie.
Kom ons begin met die skryf van sommige nutsdienste:
Ons sal eintlik 'n webwerflys nodig hê om ons stelsel uit te probeer. Skep jou eie lys of gebruik die myn:
Normaalweg hou jy hierdie lys in 'n databasis saam met die kontakbesonderhede van die eienaar sodat jy dit kan kontak. Aangesien dit nie die hoof onderwerp van hierdie tutoriaal is nie, en ter wille van eenvoud, gaan ons net hierdie Python-lys gebruik.
As jy baie goeie aandag betaal het, het jy dalk twee baie lang domeine opgemerk in die lys wat nie geldige webwerwe is nie (ek hoop niemand het hulle gekoop teen die tyd dat jy dit lees om my verkeerd te bewys nie!). Ek het hierdie twee domeine bygevoeg om seker te wees dat ons elke keer 'n paar webwerwe het. Kom ons noem ons program UptimeSquirrel.
Eerstens, laat ons die seriële benadering probeer en sien hoe swak dit presteer. Ons sal dit as die basislyn beskou.
Ons gaan 'n bietjie meer kreatief raak met die implementering van die skroefbenadering. Ons gebruik 'n tou om die adresse in te stel en werkersdrade te skep om hulle uit die tou te kry en dit te verwerk. Ons gaan wag dat die tou leeg is, wat beteken dat al die adresse deur ons werkersdrade verwerk is.
Soos voorheen genoem, is konkurrent.futures 'n hoëvlak API vir die gebruik van drade. Die benadering wat ons hier neem, impliseer dat u 'n ThreadPoolExecutor gebruik. Ons gaan take aan die swembad oorhandig en terugkry, wat resultate is wat ons in die toekoms beskikbaar sal stel. Natuurlik kan ons wag vir alle toekoms om werklike resultate te word.
Die veelverwerkende benadering
Die multiprocessing-biblioteek bied 'n byna inskakel-API vir die threading-biblioteek. In hierdie geval gaan ons 'n benadering volg wat soortgelyk is aan die konkurrent.future een. Ons stel 'n multiprocessing op. Stel take in en stuur dit deur 'n funksie na die adreslys te karwei (dink aan die klassieke Python-kaartfunksie).
Gevent is 'n gewilde alternatief vir die bereiking van massiewe gelyktydigheid. Daar is 'n paar dinge wat jy moet weet voordat jy dit gebruik:
Kode wat gelyktydig deur groenjies uitgevoer word, is deterministies. In teenstelling met die ander alternatiewe wat aangebied word, verseker hierdie paradigma dat jy vir enige twee identiese lopies altyd dieselfde resultate sal kry in dieselfde volgorde.
Jy moet standaardfunksies van aapvlek gebruik sodat hulle saamwerk met gevent. Hier is wat ek daarmee bedoel. Normaalweg blokkeer 'n sokbewerking. Ons wag vir die operasie om af te handel. As ons in 'n multithreaded omgewing was, sou die skeduleerder net oorskakel na 'n ander draad terwyl die ander een vir I / O wag. Aangesien ons nie in 'n multithreaded omgewing is nie, het ons die standaard funksies geventileer sodat dit nie-blokkeer word en die beheer na die geprogrammeerde skeduleerder terugbesorg.
Om te installeer, moet jy hardloop: pip installasie gevent
Gaan soos volg te werk om ons taak uit te voer met behulp van 'n gevent.pool.Pool:
Seldery is 'n benadering wat meestal verskil van wat ons tot dusver gesien het. Dit word geveg getoets in die konteks van baie komplekse en hoë-prestasie omgewings. Die opstel van Seldery benodig 'n bietjie meer tinkering as al die bogenoemde oplossings.
Eerstens moet ons Seldery installeer:
peper installeer seldery
Take is die sentrale konsepte in die Seldery-projek. Alles wat jy in Seldery wil hardloop, moet 'n taak wees. Seldery bied groot buigsaamheid vir lopende take: jy kan dit sinchronies of asynchroon, in reële tyd of geskeduleer, op dieselfde masjien of op verskeie masjiene hardloop, en met behulp van drade, prosesse, Eventlet of gevent.
Die reëling sal effens meer kompleks wees. Seldery gebruik ander dienste vir die stuur en ontvang van boodskappe. Hierdie boodskappe is gewoonlik take of resultate van take. Ons gaan vir hierdie doel Redis in hierdie handleiding gebruik. Redis is 'n goeie keuse, want dit is baie maklik om te installeer en te konfigureer. Dit is regtig moontlik om dit reeds in jou aansoek vir ander doeleindes te gebruik, soos caching en pub / sub.
U kan Redis installeer deur die instruksies op die Redis Quick Start-bladsy te volg. Moenie vergeet om die Redis Python-biblioteek te installeer nie, pip install redis, en die bundel wat nodig is vir die gebruik van Redis en Seldery: Pip Installeer seldery [Redis].
Begin die Redis-bediener soos volg: $ redis-bediener
Om dinge met Seldery te begin bou, moet ons eers 'n Seldery-aansoek maak. Daarna moet Seldery weet watter soort take dit mag uitvoer. Om dit te bereik, moet ons take in die seldery aansoek registreer. Ons sal dit doen met behulp van die @ app.task-versierder:
Moenie paniekerig raak as daar niks gebeur nie. Onthou, Seldery is 'n diens, en ons moet dit hardloop. Tot nou toe het ons net die take in Redis geplaas, maar het nie Seldery begin om hulle uit te voer nie. Om dit te kan doen, moet ons hierdie opdrag uitvoer in die gids waar ons kode woonagtig is:
seldery werker -A do_celery -loglevel = debug -concurrency = 4
Herleef nou die Python-skrif en kyk wat gebeur. Een ding om aandag te skenk aan: Let op hoe ons die Redis-adres twee keer aan ons Redis-aansoek oorgedra het. Die makelaar parameter spesifiseer waar die take geselekteer word, en agtergrond is waar Seldery die resultate plaas sodat ons dit in ons app kan gebruik. As ons nie 'n resultaat terugvoer spesifiseer nie, kan ons nie weet wanneer die taak verwerk is en wat die resultaat was nie.
Wees ook bewus daarvan dat die logs nou in die standaard uitset van die Seldery-proses is, dus maak seker dat hulle in die toepaslike terminaal gekontroleer word.
Ek hoop dit was 'n interessante reis vir jou en 'n goeie inleiding tot die wêreld van parallelle / gelyktydige programmering in Python. Dit is die einde van die reis, en daar is 'n paar gevolgtrekkings wat ons kan teken:
Daar is verskeie paradigmas wat ons help om hoëprestasie-rekenaars in Python te bereik.
Vir die multi-threaded paradigma, het ons die threading en concurrent.futures biblioteke.
Multiprocessing bied 'n baie soortgelyke koppelvlak aan die draad, maar vir prosesse eerder as drade.
Onthou dat prosesse ware parallelisme bereik, maar dit is duurder om te skep.
Onthou dat 'n proses meer drade kan hê wat daarin loop.
Moenie parallel vergelyk vir vergelyking nie. Onthou dat slegs die parallelle benadering voordeel trek uit multikernprosessors, terwyl gelyktydige programmering take opdragte opdrag gee sodat wag op langlopende bedrywighede gedoen word terwyl dit ewewydig die werklike berekening doen.