Mitme lõime sisse lülitamine Python näitega: õppige GIL sisse Python
Mis on lõim?
Lõim on samaaegse programmeerimise täitmisüksus. Multithreading on tehnika, mis võimaldab CPU-l täita ühe protsessi paljusid ülesandeid korraga. Neid lõime saab käitada eraldi, jagades samal ajal oma protsessiressursse.
Mis on protsess?
Protsess on põhimõtteliselt käivitatav programm. Kui käivitate arvutis rakenduse (nt brauseris või tekstiredaktoris), loob operatsioonisüsteem a protsessi.
Mis on multithreading Python?
Mitme lõime sisse lülitamine Python programmeerimine on hästi tuntud tehnika, mille puhul protsessi mitu lõime jagavad oma andmeruumi põhilõimega, mis muudab teabe jagamise ja lõimesisese suhtluse lihtsaks ja tõhusaks. Niidid on kergemad kui protsessid. Mitut lõime võib protsessiressursse jagades käitada eraldi. Mitme lõimega töötlemise eesmärk on käivitada korraga mitu ülesannet ja funktsioonilahtrit.
Mis on multitöötlus?
Mitmeprotsessiline töötlemine võimaldab korraga käivitada mitut sõltumatut protsessi. Need protsessid ei jaga oma ressursse ja suhtlevad IPC kaudu.
Python Mitmelõimeline vs mitmetöötlus
Protsesside ja lõimede mõistmiseks kaaluge järgmist stsenaariumi: teie arvutis olev exe-fail on programm. Kui avate selle, laadib OS selle mällu ja protsessor käivitab selle. Praegu töötavat programmi eksemplari nimetatakse protsessiks.
Igal protsessil on kaks põhikomponenti:
- Kood
- Andmed
Nüüd võib protsess sisaldada ühte või mitut alamosa, mida nimetatakse niidid. See oleneb OS-i arhitektuurist. Lõime võib pidada protsessi osaks, mida operatsioonisüsteem saab eraldi käivitada.
Teisisõnu, see on juhiste voog, mida OS saab iseseisvalt käivitada. Ühe protsessi lõimed jagavad selle protsessi andmeid ja on loodud paralleelsuse hõlbustamiseks koos töötama.
Miks kasutada Multithreadingut?
Multithreading võimaldab jagada rakenduse mitmeks alamülesandeks ja käivitada neid ülesandeid samaaegselt. Kui kasutate mitut lõime õigesti, saab teie rakenduse kiirust, jõudlust ja renderdamist parandada.
Python MultiThreading
Python toetab nii multitöötluse kui ka mitme lõimega töötlemise konstruktsioone. Selles õpetuses keskendute peamiselt rakendamisele mitmelõimeline rakendused pythoniga. Seal on kaks peamist moodulit, mida saab kasutada lõimede käsitlemiseks Python:
- . niit moodul ja
- . keermestamine moodul
Kuid pythonis on ka midagi, mida nimetatakse globaalseks interpretaatori lukuks (GIL). See ei võimalda palju jõudlust suurendada ja võib isegi vähendama mõne mitme lõimega rakenduse jõudlus. Sellest kõigest saate teada selle õpetuse tulevastest osadest.
Keerme ja lõime moodulid
Kaks moodulit, mille kohta selles õpetuses tutvute, on keerme moodul ja keermestusmoodul.
Keermemoodul on aga juba ammu aegunud. Alustades sellest Python 3, see on määratud aegunuks ja on juurdepääsetav ainult kui __niit tagasiühilduvuse tagamiseks.
Peaksite kasutama kõrgemat taset keermestamine moodul rakenduste jaoks, mida kavatsete juurutada. Lõimemoodulit on siin käsitletud ainult hariduslikel eesmärkidel.
Keerme moodul
Selle mooduli abil uue lõime loomise süntaks on järgmine:
thread.start_new_thread(function_name, arguments)
Olgu, nüüd olete kodeerimise alustamiseks katnud põhiteooria. Niisiis, avage oma IDLE või märkmikusse ja tippige järgmine tekst:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Salvestage fail ja vajutage programmi käivitamiseks klahvi F5. Kui kõik tehti õigesti, peaksite nägema järgmist väljundit:
Võistlustingimuste ja nende käsitlemise kohta saate lisateavet järgmistest osadest
KOODI SELGITUS
- Need avaldused impordivad aja ja lõime mooduli, mida kasutatakse täitmise ja viivituse käsitlemiseks Python niidid.
- Siin olete määratlenud funktsiooni nimega thread_test, mida kutsutakse start_new_thread meetod. Funktsioon käivitab ajatsükli nelja iteratsiooni jooksul ja prindib seda kutsunud lõime nime. Kui iteratsioon on lõppenud, prindib see teate, et lõim on täitmise lõpetanud.
- See on teie programmi põhiosa. Siin helistate lihtsalt start_new_thread meetodiga lõime_test funktsioon argumendina. See loob argumendina edastatava funktsiooni jaoks uue lõime ja alustab selle täitmist. Pange tähele, et saate selle (lõime_test) mis tahes muu funktsiooniga, mida soovite lõimena käivitada.
Keermestamise moodul
See moodul on keermestamise kõrgetasemeline rakendamine Pythonis ja de facto standard mitme lõimega rakenduste haldamiseks. Võrreldes keermemooduliga pakub see laia valikut funktsioone.
Siin on nimekiri mõnest selles moodulis määratletud kasulikest funktsioonidest:
Funktsiooni nimi | Kirjeldus |
---|---|
activeCount() | Tagastab arvu Keere objektid, mis on veel elus |
currentThread() | Tagastab klassi Thread praeguse objekti. |
loetlema () | Loetleb kõik aktiivsed lõimeobjektid. |
isDaemon() | Tagastab tõene, kui lõim on deemon. |
on elus() | Tagastab tõe, kui niit on veel elus. |
Lõim klassi meetodid | |
start () | Alustab lõime tegevust. Seda tuleb iga lõime jaoks kutsuda ainult üks kord, kuna mitmekordsel kutsumisel ilmneb käitustõrge. |
jooksma () | See meetod tähistab lõime aktiivsust ja seda saab alistada klass, mis laiendab lõime klassi. |
liitu () | See blokeerib muu koodi täitmise, kuni lõim, millel meetodit join() kutsuti, lõpetatakse. |
Taustalugu: lõime klass
Enne mitmelõimega programmide kodeerimise alustamist lõimemismooduli abil on oluline mõista Thread klassi. Lõimeklass on põhiklass, mis määrab Pythonis lõime malli ja toimingud.
Kõige tavalisem viis mitme lõimega pythoni rakenduse loomiseks on klassi deklareerimine, mis laiendab Thread klassi ja alistab selle run() meetodi.
Kokkuvõttes tähistab klass Thread koodijada, mis jookseb eraldi niit kontroll.
Nii et mitme lõimega rakenduse kirjutamisel teete järgmist.
- Määrake klass, mis laiendab lõime klassi
- Alistage __init__ konstruktor
- Alistage jooksma () meetod
Kui niidiobjekt on tehtud, siis start () meetodit saab kasutada selle tegevuse elluviimise alustamiseks ja liitu () meetodit saab kasutada kogu muu koodi blokeerimiseks kuni praeguse tegevuse lõpuni.
Nüüd proovime kasutada keermestusmoodulit, et rakendada teie eelmist näidet. Jällegi süütage oma IDLE ja sisestage järgmine tekst:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
See on väljund, kui käivitate ülaltoodud koodi:
KOODI SELGITUS
- See osa on sama, mis meie eelmine näide. Siin impordite aja ja lõime mooduli, mida kasutatakse täitmise ja viivituste käsitlemiseks Python niidid.
- Selles bitis loote klassi nimega threadtester, mis pärib või laiendab Keere keermestusmooduli klass. See on üks levinumaid viise pythonis lõimede loomiseks. Siiski peaksite alistama ainult konstruktori ja jooksma () meetodit oma rakenduses. Nagu näete ülaltoodud koodinäidisest, __init__ meetod (konstruktor) on alistatud. Samamoodi olete alistanud ka jooksma () meetod. See sisaldab koodi, mida soovite lõimes käivitada. Selles näites olete kutsunud funktsiooni thread_test().
- See on meetod thread_test(), mis võtab väärtuse i argumendina vähendab seda igal iteratsioonil 1 võrra ja liigub läbi ülejäänud koodi, kuni i-st saab 0. Igas iteratsioonis prindib see parajasti käivitatava lõime nime ja jääb sekunditeks magama (mida võetakse ka argumendina ).
- thread1 = threadtester(1, “First Thread”, 1) Siin loome lõime ja edastame kolm parameetrit, mille deklareerisime failis __init__. Esimene parameeter on lõime id, teine parameeter on lõime nimi ja kolmas parameeter on loendur, mis määrab, mitu korda while tsükkel peaks jooksma.
- thread2.start()T algusmeetodit kasutatakse lõime täitmise alustamiseks. Sisemiselt kutsub start() funktsioon välja sinu klassi meetodi run().
- thread3.join() Meetod join() blokeerib muu koodi täitmise ja ootab, kuni lõim, millel seda kutsuti, lõpeb.
Nagu te juba teate, on samas protsessis olevatel lõimedel juurdepääs selle protsessi mälule ja andmetele. Selle tulemusena, kui mitu lõime üritab andmeid korraga muuta või neile juurde pääseda, võivad sisse hiilida vead.
Järgmises jaotises näete erinevaid tüsistusi, mis võivad ilmneda, kui lõimed pääsevad juurde andmetele ja kriitilisele jaotisele, ilma olemasolevaid juurdepääsutehinguid kontrollimata.
Ummikseisud ja võistlustingimused
Enne ummikseisude ja võistlustingimuste tundmaõppimist on kasulik mõista mõnda samaaegse programmeerimisega seotud põhimääratlust.
- Kriitiline osaSee on koodifragment, mis pääseb juurde või muudab jagatud muutujaid ja mida tuleb sooritada tuumatehinguna.
- KontekstivahetusSee on protsess, mida CPU järgib lõime oleku salvestamiseks enne ühelt ülesandelt teisele üleminekut, et seda saaks hiljem samast punktist jätkata.
Ummikud
Ummikud on enim kardetud probleem, millega arendajad pythonis samaaegseid/mitmelõimelisi rakendusi kirjutades kokku puutuvad. Parim viis ummikseisude mõistmiseks on kasutada klassikalist arvutiteaduse näidet, mida nimetatakse Söögi- Philosophers Probleem.
Söögifilosoofide probleemipüstitus on järgmine:
Viis filosoofi istuvad ümarlaual, millel on viis taldrikut spagette (teatud tüüpi pasta) ja viis kahvlit, nagu joonisel näidatud.
Filosoof peab igal ajahetkel kas sööma või mõtlema.
Veelgi enam, filosoof peab võtma kaks temaga külgnevat kahvlit (st vasaku ja parema kahvli), enne kui ta saab spagette süüa. Ummikuprobleem tekib siis, kui kõik viis filosoofi võtavad korraga oma õiged kahvlid.
Kuna igal filosoofil on üks kahvel, ootavad nad kõik, kuni teised kahvli maha panevad. Selle tulemusena ei saa ükski neist spagette süüa.
Samamoodi tekib samaaegses süsteemis ummik, kui erinevad lõimed või protsessid (filosoofid) üritavad korraga omandada jagatud süsteemiressursse (kahvleid). Selle tulemusena ei saa ükski protsess käivitada, kuna nad ootavad mõnda muud ressurssi, mida hoiab mõni muu protsess.
Võistlustingimused
Võistlusseisund on programmi soovimatu olek, mis tekib siis, kui süsteem sooritab korraga kahte või enamat toimingut. Näiteks kaaluge seda lihtsat tsüklit:
i=0; # a global variable for x in range(100): print(i) i+=1;
Kui loote n seda koodi korraga käivitavate lõimede arv, ei saa te määrata i väärtust (mida lõimed jagavad), kui programm lõpetab täitmise. Selle põhjuseks on asjaolu, et tõelises mitmelõimelises keskkonnas võivad lõimed kattuda ning lõime poolt välja otsitud ja muudetud i väärtus võib vahepeal muutuda, kui mõni teine lõim sellele juurde pääseb.
Need on kaks peamist probleemide klassi, mis võivad ilmneda mitme lõimega või hajutatud pythoni rakenduses. Järgmises jaotises saate teada, kuidas seda probleemi lõime sünkroonides ületada.
Synchroniseerivad niidid
Võistlustingimuste, ummikseisude ja muude keermepõhiste probleemide lahendamiseks pakub keermestusmoodul järgmist lukk objektiks. Idee seisneb selles, et kui lõim soovib juurdepääsu konkreetsele ressursile, omandab see selle ressursi lukustuse. Kui lõim lukustab teatud ressursi, ei pääse ükski teine lõim sellele juurde enne, kui lukk on vabastatud. Selle tulemusena on muudatused ressursis tuumakad ja rassitingimused välditakse.
Lukk on madala taseme sünkroonimisprimitiiv, mille on rakendanud __niit moodul. Igal ajahetkel võib lukk olla ühes kahest olekust: lukus or lukustamata. See toetab kahte meetodit:
- omandama ()Kui lukustatav olek on lukustamata, muudab hankimise() meetodi kutsumine oleku lukustatud ja naaseb. Kui aga olek on lukus, blokeeritakse kutse hankimiseks () seni, kuni mõni muu lõim kutsub välja release() meetodi.
- vabasta ()Release() meetodit kasutatakse oleku määramiseks lukustamata, st luku vabastamiseks. Seda saab kutsuda mis tahes lõimega, mitte tingimata sellega, mis luku omandas.
Siin on näide lukkude kasutamisest oma rakendustes. Süüta oma IDLE ja tippige järgmine:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Nüüd vajuta F5. Peaksite nägema sellist väljundit:
KOODI SELGITUS
- Siin loote lihtsalt uue luku, helistades numbrile keermestamine.Lock() tehase funktsioon. Sisemiselt tagastab Lock() kõige tõhusama konkreetse lukustusklassi eksemplari, mida platvorm hooldab.
- Esimeses lauses omandate luku, kutsudes välja hankimismeetodi. Kui lukk on antud, printige "Lukk omandatud" konsooli juurde. Kui kogu kood, mida soovite lõime käivitada, on täitmise lõpetanud, vabastate lukustuse, kutsudes välja meetodi release().
Teooria on hea, aga kuidas sa tead, et lukk tõesti töötas? Kui vaatate väljundit, näete, et iga prindilause prindib täpselt ühe rea korraga. Tuletage meelde, et varasemas näites olid printimise väljundid juhuslikud, kuna mitu lõime pääses korraga juurde print() meetodile. Siin käivitatakse printimisfunktsioon alles pärast lukustuse omandamist. Seega kuvatakse väljundid ükshaaval ja ridahaaval.
Lisaks lukkudele toetab python ka mõnda muud mehhanismi lõime sünkroonimiseks, nagu on loetletud allpool:
- RLukud
- Semaphores
- Tingimused
- Sündmused ja
- Piirded
Global Interpreter Lock (ja kuidas sellega toime tulla)
Enne pythoni GIL-i üksikasjadesse laskumist määratleme mõned terminid, mis on tulevase jaotise mõistmisel kasulikud:
- CPU-ga seotud kood: see viitab mis tahes koodilõigule, mida protsessor otse käivitab.
- I/O-sidestatud kood: see võib olla mis tahes kood, mis pääseb failisüsteemile OS-i kaudu
- CPython: see on viide täitmine of Python ja seda saab kirjeldada kui tõlgi, mis on kirjutatud C ja Python (programmeerimiskeel).
Milles GIL on Python?
Global Interpreter Lock (GIL) pythonis on protsessilukk või mutex, mida kasutatakse protsesside käsitlemisel. See tagab, et üks lõim pääseb korraga juurde konkreetsele ressursile ning takistab ka objektide ja baitkoodide korraga kasutamist. See toob kasu ühe lõimega programmidele jõudluse suurendamisel. GIL pythonis on väga lihtne ja hõlpsasti rakendatav.
Luku abil saab tagada, et teatud ressursile on teatud ajahetkel juurdepääs ainult ühel lõimel.
Üks omadusi Python seisneb selles, et see kasutab iga tõlgendusprotsessi puhul globaalset lukku, mis tähendab, et iga protsess käsitleb pythoni interpretaatorit ennast ressurssina.
Oletagem näiteks, et olete kirjutanud pythoni programmi, mis kasutab kahte lõime nii protsessori kui ka I/O toimingute tegemiseks. Selle programmi käivitamisel juhtub see:
- Pythoni tõlk loob uue protsessi ja loob lõimed
- Kui lõime 1 hakkab tööle, omandab see esmalt GIL-i ja lukustab selle.
- Kui lõime 2 soovib praegu käivitada, peab see ootama GIL-i vabastamist, isegi kui mõni teine protsessor on vaba.
- Oletame nüüd, et lõime-1 ootab I/O toimingut. Sel ajal vabastab see GIL-i ja lõime-2 omandab selle.
- Kui lõime 1 soovib pärast I/O operatsioonide lõpetamist kohe käivituda, peab see taas ootama, kuni lõime 2 vabastab GIL-i.
Seetõttu pääseb interpretaatorile igal ajal juurde ainult üks lõim, mis tähendab, et pythoni koodi käivitab teatud ajahetkel ainult üks lõim.
Ühetuumalise protsessori puhul on see kõik korras, kuna see kasutaks lõimede käsitlemiseks ajalõikamist (vt selle õpetuse esimest jaotist). Kuid mitmetuumaliste protsessorite puhul mõjutab mitmel lõimel töötav protsessoriga seotud funktsioon programmi tõhusust märkimisväärselt, kuna see ei kasuta tegelikult kõiki saadaolevaid tuumasid korraga.
Miks oli GIL-i vaja?
CPython prügikoguja kasutab tõhusat mäluhaldustehnikat, mida nimetatakse viiteloenduseks. See toimib järgmiselt: igal pythoni objektil on viidete arv, mida suurendatakse, kui sellele omistatakse uus muutuja nimi või lisatakse konteinerisse (nt korteežid, loendid jne). Samamoodi vähendatakse viidete arvu, kui viide väljub rakendusalast või kui kutsutakse välja käsk del. Kui objekti viitearv jõuab 0-ni, kogutakse see prügi ja eraldatud mälu vabastatakse.
Kuid probleem on selles, et võrdlusarvu muutuja on kalduvus rassitingimustele nagu mis tahes muu globaalne muutuja. Selle probleemi lahendamiseks otsustasid pythoni arendajad kasutada globaalset tõlgilukku. Teine võimalus oli lisada igale objektile lukk, mis oleks toonud kaasa ummikseisu ja suurendanud hankimise () ja release () kõnede lisakulusid.
Seetõttu on GIL märkimisväärne piirang mitme lõimega Pythoni programmidele, mis käitavad raskeid protsessoriga seotud toiminguid (muutes need tõhusalt ühelõimeliseks). Kui soovite oma rakenduses kasutada mitut CPU südamikku, kasutage multitöötlus mooduli asemel.
kokkuvõte
- Python toetab 2 moodulit mitme lõimega töötlemiseks:
- __niit moodul: see pakub keermestamiseks madalat rakendust ja on vananenud.
- keermestusmoodul: See pakub mitme lõimega töötlemise kõrgetasemelist teostust ja on praegune standard.
- Lõime loomiseks keermestusmooduli abil peate tegema järgmist.
- Loo klass, mis laiendab Keere klass.
- Alista selle konstruktor (__init__).
- Alista see jooksma () meetod.
- Looge selle klassi objekt.
- Lõimi saab käivitada helistades start () meetod.
- . liitu () meetodit saab kasutada teiste lõimede blokeerimiseks, kuni see lõim (see, millel liitumine kutsuti) täitmise lõpetab.
- Võistlusseisund ilmneb siis, kui mitu lõime pääseb samaaegselt juurde jagatud ressursile või muudab seda.
- Seda saab vältida, Synchroniseerivad niidid.
- Python toetab lõime sünkroonimiseks 6 võimalust:
- Lukud
- RLukud
- Semaphores
- Tingimused
- Sündmused ja
- Piirded
- Lukud lubavad kriitilisse sektsiooni siseneda ainult teatud lõimel, mis on luku saanud.
- Lukul on kaks peamist meetodit:
- omandama (): seab lukustuse olekusse lukus. Lukustatud objekti kutsumisel blokeerub see seni, kuni ressurss on vaba.
- vabasta (): seab lukustuse olekusse lukustamata ja naaseb. Kui kutsutakse lukustamata objektile, tagastab see vale.
- Globaalne tõlgi lukk on mehhanism, mille kaudu ainult 1 CPython tõlgiprotsess saab käivituda korraga.
- Seda kasutati C viiteloendusfunktsiooni hõlbustamiseksPythons prügivedaja.
- Et Python rakenduste puhul, millel on raske protsessoriga seotud toimingud, peaksite kasutama multitöötlusmoodulit.