Többszálas szál Python-ban globális tolmács-zár (GIL) példával

Tartalomjegyzék:

Anonim

A python programozási nyelv lehetővé teszi többprocesszoros vagy többszálas használatát. Ebben az oktatóanyagban megtanulhatja, hogyan kell többszálú alkalmazásokat írni Pythonban.

Mi az a szál?

A szál az egyidejű programozás exekciója. A többszálas szálak egy olyan technika, amely lehetővé teszi a CPU számára, hogy egy folyamat során sok feladatot hajtson végre egyszerre. Ezek a szálak egyenként futtathatók, miközben megosztják a folyamat erőforrásaikat.

Mi az a folyamat?

A folyamat alapvetően a végrehajtott program. Amikor elindít egy alkalmazást a számítógépén (például böngésző vagy szövegszerkesztő), az operációs rendszer létrehoz egy folyamatot.

Mi az a többszálas szál a Pythonban?

A többszálas szálak Python programozásában jól ismert technika, amelyben egy folyamat több szála megosztja az adatterületét a fő szálral, ami megkönnyíti és hatékonyabbá teszi a szálakon belüli információmegosztást és kommunikációt. A szálak könnyebbek, mint a folyamatok. A több szál külön-külön is végrehajtható, miközben megosztják a folyamat erőforrásait. A többszálas kezelés célja több feladat és funkciócella futtatása egyszerre.

Mi az a többprocesszoros?

A többprocesszoros működés lehetővé teszi több egymással nem összefüggő folyamat egyidejű futtatását. Ezek a folyamatok nem osztják meg erőforrásaikat, és az IPC-n keresztül kommunikálnak.

Python Multithreading vs Multiprocessing

A folyamatok és a szálak megértéséhez vegye figyelembe ezt a forgatókönyvet: A számítógépen található .exe fájl egy program. Amikor kinyitja, az operációs rendszer betölti a memóriába, és a CPU végrehajtja. A most futó program példányát folyamatnak nevezzük.

Minden folyamatnak két alapvető eleme lesz:

  • A kód
  • Az adat

Most egy folyamat tartalmazhat egy vagy több részet, úgynevezett szálakat. Ez az operációs rendszer architektúrájától függ. Gondolhat egy szálra, mint a folyamat szakaszára, amelyet az operációs rendszer külön végrehajthat.

Más szavakkal, ez egy utasításfolyam, amelyet az operációs rendszer függetlenül futtathat. Az egyetlen folyamatban lévő szálak megosztják a folyamat adatait, és úgy vannak kialakítva, hogy együtt működjenek a párhuzamosság elősegítése érdekében.

Ebben az oktatóanyagban megtanulod,

  • Mi az a szál?
  • Mi az a folyamat?
  • Mi az a többszálas szál?
  • Mi az a többprocesszoros?
  • Python Multithreading vs Multiprocessing
  • Miért érdemes használni a többszálas szálat?
  • Python MultiThreading
  • A Menet és a Menet modulok
  • A szálmodul
  • A menetes modul
  • Holtpontok és versenyfeltételek
  • Szálak szinkronizálása
  • Mi az a GIL?
  • Miért volt szükség GIL-re?

Miért érdemes használni a többszálas szálat?

A többszálas szálak lehetővé teszik, hogy egy alkalmazást több részfeladatra bontson, és ezeket a feladatokat egyidejűleg futtassa. Ha megfelelően használja a többszálas szálat, az alkalmazás sebessége, teljesítménye és renderelése javítható.

Python MultiThreading

A Python támogatja a konstrukciókat mind a többprocesszoros, mind a többszálas feldolgozáshoz. Ebben az oktatóanyagban elsősorban a többszálas alkalmazások Python-nal történő megvalósítására összpontosít . Két fő modul használható a szálak kezelésére a Pythonban:

  1. A szálmodul , és
  2. A menetes modul

A pythonban azonban van egy úgynevezett globális tolmács-zár (GIL) is. Nem tesz lehetővé nagy teljesítménynövekedést, sőt egyes többszálas alkalmazások teljesítményét is csökkentheti . Mindent megtudhat erről az oktatóanyag következő szakaszaiban.

A Menet és a Menet modulok

A két modul, amelyről megismerheti az oktatóanyagot, a szálmodul és a szálmodul .

A szálmodul azonban már régen elavult. A Python 3-tól kezdve elavultnak minősítették, és csak a visszamenőleges kompatibilitás érdekében érhető el __thread néven .

A telepíteni kívánt alkalmazásokhoz használja a magasabb szintű szálmodult . A szálmodulra itt csak oktatási célokra került sor.

A szálmodul

Az új szál létrehozásának szintaxisa a modul segítségével a következő:

thread.start_new_thread(function_name, arguments)

Rendben, most áttekintette a kódolás megkezdésének alapelméletét. Tehát nyissa meg az IDLE vagy egy jegyzettömböt, és írja be a következőt:

import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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))

Mentse a fájlt, és nyomja meg az F5 billentyűt a program futtatásához. Ha mindent helyesen végeztek, akkor ezt a kimenetet kell látnia:

A következő szakaszokban többet megtudhat a versenykörülményekről és azok kezeléséről

KÓDMagyarázat

  1. Ezek az utasítások importálják a Python szálak végrehajtásának és késleltetésének kezelésére használt idő és szál modult.
  2. Itt definiált egy thread_test nevű függvényt, amelyet a start_new_thread metódus hív meg . A függvény egy darab ciklust futtat négy ismétlésnél, és kinyomtatja annak a szálnak a nevét, amely hívta. Miután az iteráció befejeződött, kiír egy üzenetet arról, hogy a szál befejezte a végrehajtást.
  3. Ez a program fő szakasza. Itt egyszerűen meghívja a start_new_thread metódust argumentumként a thread_test függvénnyel.

    Ez új szálat hoz létre az argumentumként átadott függvény számára, és elkezdi végrehajtani. Ne feledje, hogy ezt (szál _ teszt) bármely más funkcióval helyettesítheti, amelyet szálként szeretne futtatni.

A menetes modul

Ez a modul a szálak magas szintű megvalósítása a pythonban, és a de facto szabvány a többszálas alkalmazások kezeléséhez. A szálmodullal összehasonlítva számos funkcióval rendelkezik.

Menetmodul felépítése

Az alábbiakban felsoroljuk a modulban definiált néhány hasznos funkciót:

Funkció neve Leírás
activeCount () Visszaadja a még élő Thread objektumok számát
currentThread () Visszaadja a Szál osztály aktuális objektumát.
felsorolja () Felsorolja az összes aktív szál objektumot.
isDaemon () Igaz, ha a szál démon.
életben van() Visszatér, ha a szál még életben van.
Menetosztály módszerek
Rajt() Elindítja egy szál tevékenységét. Minden szálnál csak egyszer kell meghívni, mert futásidejű hibát dob, ha többször meghívja.
fuss() Ez a módszer egy szál aktivitását jelöli, és felülírhatja egy olyan osztály, amely kiterjeszti a Szál osztályt.
csatlakozik() Addig blokkolja más kódok végrehajtását, amíg a szál, amelyen a join () metódust meghívták, le nem áll.

Háttér: A szál osztály

Mielőtt elkezdené a többszálas programok kódolását a szálmodul használatával, elengedhetetlen megérteni a Thread osztályt. A thread osztály az elsődleges osztály, amely meghatározza a szál sablonját és műveleteit a pythonban.

A többszálas python alkalmazás létrehozásának leggyakoribb módja egy olyan osztály deklarálása, amely kiterjeszti a Thread osztályt és felülírja a run () metódust.

A Thread osztály összefoglalva egy olyan kódsorozatot jelöl, amely a vezérlés külön szálában fut .

Tehát egy többszálas alkalmazás írásakor a következőket kell tennie:

  1. meghatározzon egy osztályt, amely kiterjeszti a Szál osztályt
  2. Felülbírálja a __init__ konstruktort
  3. Felülírja a run () metódust

Miután elkészült egy szálobjektum, a start () metódussal meg lehet kezdeni ennek a tevékenységnek a végrehajtását, és a join () metódussal blokkolhatjuk az összes többi kódot, amíg az aktuális tevékenység be nem fejeződik.

Most próbáljuk meg a menetes modult használni az előző példa megvalósításához. Ismét gyújtsa be IDLE-jét, és írja be a következőt:

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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()

Ez lesz a kimenet a fenti kód végrehajtásakor:

KÓDMagyarázat

  1. Ez a rész megegyezik az előző példánkkal. Itt importálja a Python-szálak végrehajtásának és késleltetésének kezelésére használt idő- és szálmodult.
  2. Ebben a bitben egy threadtester nevű osztályt hoz létre, amely örökli vagy kiterjeszti a threading modul Thread osztályát. Ez a szálak létrehozásának egyik leggyakoribb módja a pythonban. Azonban csak a konstruktort és a run () metódust kell felülírnia az alkalmazásban. Amint a fenti kódmintából láthatja, a __init__ metódust (konstruktort) felülírták.

    Hasonlóképpen felülírta a run () metódust is. Ez azt a kódot tartalmazza, amelyet futtatni szeretne egy szálon belül. Ebben a példában meghívta a thread_test () függvényt.

  3. Ez a thread_test () metódus, amely az i értéket veszi fel argumentumként, minden iterációban 1-gyel csökkenti, és a kód többi részén végiggörgeti, amíg i nulla lesz. Minden iterációban kinyomtatja az éppen futó szál nevét és várakozási másodperceket alszik (amit szintén érvnek vesznek).
  4. thread1 = threadtester (1, "Első szál", 1)

    Itt létrehozunk egy szálat, és átadjuk a __init__-ban deklarált három paramétert. Az első paraméter a szál azonosítója, a második paraméter a szál neve, a harmadik paraméter pedig a számláló, amely meghatározza, hogy a while ciklusnak hányszor kell futnia.

  5. thread2.start ()

    A start metódus egy szál végrehajtásának megkezdésére szolgál. Belsőleg a start () függvény meghívja az osztály run () metódusát.

  6. thread3.join ()

    A join () metódus blokkolja más kódok végrehajtását és megvárja, amíg a szál, amelyen hívták, befejeződik.

Mint már tudják, az ugyanabban a folyamatban lévő szálak hozzáférnek a folyamat memóriájához és adataihoz. Ennek eredményeként, ha egynél több szál egyszerre próbálja megváltoztatni vagy elérni az adatokat, hibák kúszhatnak be.

A következő szakaszban megnézheti azokat a különféle bonyodalmakat, amelyek akkor jelentkezhetnek, amikor a szálak hozzáférnek az adatokhoz és a kritikus szakaszhoz anélkül, hogy ellenőriznék a meglévő hozzáférési tranzakciókat.

Holtpontok és versenyfeltételek

Mielőtt megismerné a holtpontokat és a versenykörülményeket, hasznos megérteni néhány, az egyidejű programozással kapcsolatos alapvető meghatározást:

  • Kritikus szakasz

    Ez egy olyan kódtöredék, amely hozzáférést biztosít vagy módosít a megosztott változókhoz, és atomi tranzakcióként kell végrehajtani.

  • Környezetváltás

    Ez az a folyamat, amelyet a CPU követ egy szál állapotának tárolására, mielőtt az egyik feladattól a másikig váltana, hogy később ugyanabból a pontról folytathassa.

Holtpontok

A holtpontok a legfélelmetesebb probléma, amellyel a fejlesztők szembesülnek, amikor egyidejű / többszálú alkalmazásokat írnak a pythonba. A holtpontokat a Dining Philosophers Problem néven ismert klasszikus számítástechnikai példaprobléma alkalmazásával lehet legjobban megérteni .

Az étkezési filozófusok problémamegállapítása a következő:

Öt filozófus ül egy kerek asztalon, öt tányér spagettivel (egyfajta tészta) és öt villával, amint azt az ábra mutatja.

Étkezési filozófusok problémája

A filozófusnak bármikor vagy eszik, vagy gondolkodik.

Sőt, egy filozófusnak el kell vennie a mellette lévő két villát (azaz a bal és a jobb villát), mielőtt megeheti a spagettit. A holtpont problémája akkor jelentkezik, amikor mind az öt filozófus egyszerre veszi fel a jobb villáját.

Mivel mindegyik filozófusnak van egy villája, mindannyian megvárják, amíg a többiek leteszik a villájukat. Ennek eredményeként egyikük sem fog enni spagettit.

Hasonlóképpen, egyidejű rendszerben holtpont következik be, amikor különböző szálak vagy folyamatok (filozófusok) egyszerre próbálják megszerezni a megosztott rendszererőforrásokat (villák). Ennek eredményeként egyik folyamat sem kap esélyt a végrehajtásra, mivel egy másik folyamat által tárolt másik erőforrásra várnak.

Versenyfeltételek

A versenyfeltétel a program nem kívánt állapota, amely akkor következik be, amikor a rendszer két vagy több műveletet végez egyszerre. Vegyük például ezt a ciklusra:

i=0; # a global variablefor x in range(100):print(i)i+=1;

Ha n számú szálat hoz létre, amelyek egyszerre futtatják ezt a kódot, akkor nem tudja meghatározni az i értékét (amelyet a szálak osztanak meg), amikor a program befejezi a végrehajtást. Ez azért van, mert egy valódi többszálas környezetben a szálak átfedhetik egymást, és az i értéke, amelyet egy szál lekért és módosított, megváltozhat, amikor más szál hozzáfér hozzá.

Ez a probléma két fő osztálya fordulhat elő egy többszálas vagy elosztott python alkalmazásban. A következő szakaszban megtudhatja, hogyan lehet legyőzni ezt a problémát a szálak szinkronizálásával.

Szálak szinkronizálása

Versenyfeltételek, holtpontok és egyéb szálalapú problémák kezeléséhez a menetes modul biztosítja a Lock objektumot. Az ötlet az, hogy amikor egy szál hozzáférést akar egy adott erőforráshoz, akkor zárat szerez az adott erőforráshoz. Miután egy szál lezár egy adott erőforrást, addig egyetlen szál sem férhet hozzá, amíg a zár fel nem oldódik. Ennek eredményeként az erőforrás változásai atomi jellegűek lesznek, és elkerülhetők lesznek a versenykörülmények.

A zár egy alacsony szintű szinkronizációs primitív, amelyet a __thread modul hajt végre . Egy adott időpontban a zár a 2 állapot egyikében lehet: zárva vagy feloldva. Két módszert támogat:

  1. szerez()

    Amikor a zár-állapot fel van oldva, a megszerzés () metódus meghívása az állapotot zároltra és visszatérésre váltja. Ha azonban az állapot zárolva van, akkor a () megszerzésének hívása blokkolódik, amíg a release () metódust más szál nem hívja meg.

  2. kiadás()

    A release () metódust arra használjuk, hogy az állapotot feloldottá tegyük, azaz zárat oldjunk fel. Bármely szál hívható, nem feltétlenül az, amelyik megszerezte a zárat.

Íme egy példa a zárak alkalmazására. Indítsa el az IDLE-t, és írja be a következőt:

import threadinglock = 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()

Most nyomja meg az F5 billentyűt. Ilyen kimenetet kell látnia:

KÓDMagyarázat

  1. Itt egyszerűen létrehoz egy új zárat a threading.Lock () gyári függvény meghívásával . Belsőleg a Lock () a platform által fenntartott leghatékonyabb konkrét Lock osztály egy példányát adja vissza.
  2. Az első állításban a lock (megszerzés) módszert hívja meg. A zár megadását követően kinyomtatja a "megszerzett zárat" a konzolra. Miután a szál futtatásához szükséges összes kód végrehajtása befejeződött, a release () metódus meghívásával oldja fel a zárat.

Az elmélet rendben van, de honnan lehet tudni, hogy a zár valóban működött? Ha megnézi a kimenetet, látni fogja, hogy a nyomtatott utasítások mindegyike pontosan egy sort nyomtat egyszerre. Emlékezzünk vissza arra, hogy egy korábbi példában a nyomtatás kimenetei véletlenszerűek, mert egyszerre több szál érte el a print () metódust. Itt a nyomtatási funkció csak a zár megszerzése után hívható meg. Tehát a kimenetek egyenként és soronként jelennek meg.

A zárak mellett a python más mechanizmusokat is támogat a szálak szinkronizálásának kezelésére, az alábbiak szerint:

  1. RLocks
  2. Szemaforok
  3. Körülmények
  4. Események, és
  5. Korlátok

Globális tolmács zár (és hogyan kell kezelni)

Mielőtt rátérnénk a python GIL részleteire, definiáljunk néhány kifejezést, amelyek hasznosak lesznek a következő szakasz megértésében:

  1. CPU-hoz kötött kód: ez bármely olyan kóddarabra vonatkozik, amelyet a CPU közvetlenül végrehajt.
  2. I / O-kötött kód: ez bármilyen kód lehet, amely az operációs rendszeren keresztül hozzáfér a fájlrendszerhez
  3. CPython: ez a Python referencia- megvalósítása , és leírható C és Python nyelven (programozási nyelv) írt tolmácsként.

Mi a GIL a Pythonban?

A globális tolmács zár (GIL) a pythonban egy folyamatzár vagy egy mutex, amelyet a folyamatok kezelése során használnak. Biztosítja, hogy egy szál egyszerre férhessen hozzá egy adott erőforráshoz, és megakadályozza az objektumok és a bájtkódok használatát is egyszerre. Ez elősegíti az egyszálú programok teljesítménynövelését. A GIL a pythonban nagyon egyszerű és könnyen megvalósítható.

A zár felhasználható annak biztosítására, hogy csak egy szál férjen hozzá egy adott erőforráshoz adott időben.

A Python egyik jellemzője, hogy globális zárat használ minden tolmácsfolyamatnál, ami azt jelenti, hogy minden folyamat magát a python tolmácsot erőforrásként kezeli.

Tegyük fel például, hogy írt egy python programot, amely két szálat használ mind a CPU, mind az „I / O” műveletek végrehajtásához. A program végrehajtásakor ez történik:

  1. A python tolmács új folyamatot hoz létre és hozza létre a szálakat
  2. Amikor az 1-es szál futni kezd, először megszerzi a GIL-t és lezárja.
  3. Ha a thread-2 most végre akar hajtani, akkor meg kell várnia a GIL kiadását akkor is, ha egy másik processzor szabad.
  4. Tegyük fel, hogy az 1. szál I / O műveletre vár. Ekkor kiadja a GIL-t, és a thread-2 megszerzi.
  5. Az I / O opciók befejezése után, ha az 1-es szál most végre akarja hajtani, akkor ismét meg kell várnia a GIL felszabadítását a 2. szál által.

Emiatt bármikor csak egy szál férhet hozzá a tolmácshoz, ami azt jelenti, hogy csak egy szál fog végrehajtani python kódot egy adott időpontban.

Ez rendben van az egymagos processzorban, mert időszeletelést használ (lásd az oktatóanyag első szakaszát) a szálak kezelésére. Többmagos processzorok esetén azonban a több szálon futtatott, processzorhoz kötött funkció jelentős hatással lesz a program hatékonyságára, mivel valójában nem használja az összes elérhető magot egyszerre.

Miért volt szükség GIL-re?

A CPython szemétgyűjtő hatékony memóriakezelési technikát használ, amelyet referenciaszámlálásnak neveznek. Így működik: A python minden objektumának van referencia száma, amely növekszik, ha új változónévhez rendelik, vagy hozzáadják egy tárolóhoz (például sorok, listák stb.). Hasonlóképpen, a referenciaszám csökken, ha a referencia kilép a hatókörből, vagy amikor a del utasítást hívják meg. Amikor egy objektum referencia száma eléri a 0 értéket, szemét gyűlik össze, és a kiosztott memória felszabadul.

De a probléma az, hogy a referenciaszám változó hajlamos versenyfeltételekre, mint bármely más globális változó. A probléma megoldása érdekében a python fejlesztői úgy döntöttek, hogy a globális tolmácszárat használják. A másik lehetőség az volt, hogy minden objektumhoz hozzá kellett adni egy zárat, ami holtpontokat és megnövekedett rezsit eredményezett volna a szerzési () és a felszabadítási () hívásokból.

Ezért a GIL jelentős korlátozást jelent a többszálú python programok számára, amelyek nehéz CPU-hoz kötött műveleteket futtatnak (gyakorlatilag egyszálúvá teszik őket). Ha több CPU-magot szeretne használni az alkalmazásában, használja inkább a többprocesszoros modult.

Összegzés

  • A Python 2 modult támogat a többszálas szálakhoz:
    1. __thread modul: Alacsony szintű megvalósítást biztosít a szálakhoz és elavult.
    2. threading modul : Magas szintű megvalósítást biztosít a többszálas szálakhoz és a jelenlegi szabvány.
  • A szál létrehozásához a szálmodul segítségével a következőket kell tennie:
    1. Hozzon létre egy osztályt, amely kiterjeszti a Szál osztályt.
    2. Felülírja a konstruktort (__init__).
    3. Felülírja a run () metódust.
    4. Hozzon létre egy objektumot ebből az osztályból.
  • Egy szál végrehajtható a start () metódus meghívásával .
  • A join () metódus addig használható más szálak blokkolására, amíg ez a szál (az, amelyre a csatlakozást hívták) befejezi a végrehajtást.
  • Versenyfeltétel akkor fordul elő, ha több szál egyidejűleg hozzáfér vagy módosít egy megosztott erőforrást.
  • A szálak szinkronizálásával elkerülhető.
  • A Python 6 módon támogatja a szálak szinkronizálását:
    1. Zárak
    2. RLocks
    3. Szemaforok
    4. Körülmények
    5. Események, és
    6. Korlátok
  • A zárak csak egy adott szálat engednek be a kritikus szakaszba, amely megszerezte a zárat.
  • A Lock 2 fő módszerrel rendelkezik:
    1. megszerzés () : A zár állapotát zároltra állítja . Ha egy lezárt objektumra hívják meg, addig blokkol, amíg az erőforrás szabad lesz.
    2. release () : A zár állapotát feloldottá teszi, és visszatér. Ha egy zárolatlan objektumot hívnak meg, akkor hamis értéket ad vissza.
  • A globális tolmácszár olyan mechanizmus, amelyen keresztül egyszerre csak 1 CPython tolmácsfolyamat hajtható végre.
  • A CPythons szemétgyűjtőjének referenciaszámlálási funkciójának megkönnyítésére használták.
  • A Python-alkalmazások nehéz CPU-hoz kötött műveletekkel történő elkészítéséhez használja a többprocesszoros modult.