Többszálú bemenet Python példával: Tanulja meg a GIL-t Python
⚡ Okos összefoglaló
Többszálú bemenet Python több szálat futtat egy folyamaton belül, így azok megosztják a memóriát és egyidejűleg működnek. A szálkezelő modul létrehozza és kezeli ezeket a szálakat, míg a Global Interpreter Lock korlátozza a valódi párhuzamosságot, így a technika a legjobb a bemeneti/kimeneti feladatokhoz.

Az Python programozási nyelv lehetővé teszi a többfeldolgozást vagy többszálú működést. Ebben az oktatóanyagban megtanulod, hogyan írhatsz többszálú alkalmazásokat Python.
Mi az a szál?
A szál (thread) a párhuzamos programozás végrehajtási egysége. A többszálú feldolgozás (multithreading) egy olyan technika, amely lehetővé teszi a CPU számára, hogy egy folyamat több feladatát egyszerre hajtsa végre. Ezek a szálak egyénileg is végrehajthatók, miközben megosztják a folyamat erőforrásait.
Mi az a folyamat?
A folyamat alapvetően a végrehajtás alatt álló program. Amikor elindít egy alkalmazást a számítógépén (például egy böngészőt vagy szövegszerkesztőt), az operációs rendszer létrehoz egy folyamat.
Mi az a Multithreading Python?
Többszálú bemenet Python A programozás egy jól ismert technika, amelyben egy folyamat több szála megosztja adatterületét a fő szállal, ami egyszerűvé és hatékonnyá teszi az információmegosztást és a kommunikációt a szálakon belül. A szálak könnyebbek, mint a folyamatok. Több szál futhat külön-külön, miközben megosztják a folyamat erőforrásait. A többszálú feldolgozás célja több feladat és függvény egyidejű futtatása.
Mi az a Multiprocessing?
multiprocessing lehetővé teszi több független 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 szálak megértéséhez vegyünk egy ilyen forgatókönyvet: Egy .exe fájl a számítógépeden egy program. Amikor megnyitod, az operációs rendszer betölti a memóriába, és a CPU végrehajtja. A programnak azt a példányát, amelyik most fut, folyamatnak nevezzük.
Minden folyamatnak két alapvető összetevője van:
- Az Code
- Az adat
Most egy folyamat egy vagy több ún. alrészt tartalmazhat szálak. Ez az operációs rendszer architektúrájától függ. A szálat a folyamat egy olyan szakaszaként képzelhetjük el, amelyet az operációs rendszer külön végrehajthat.
Más szóval, ez egy olyan utasításfolyam, amelyet az operációs rendszer függetlenül futtathat. Az egyetlen folyamaton belüli szálak megosztják az adott folyamat adatait, és úgy vannak kialakítva, hogy együttműködve elősegítsék a párhuzamosságot.
Miért használja a Multithreading-et?
A multithreading lehetővé teszi, hogy egy alkalmazást több részfeladatra bontsa, és ezeket a feladatokat egyidejűleg futtassa. Ha megfelelően használja a többszálas megoldást, az alkalmazás sebessége, teljesítménye és renderelése egyaránt javítható.
Python Többszálas
Python támogatja mind a többfeldolgozású, mind a többszálú feldolgozáshoz szükséges konstrukciókat. Ebben az oktatóanyagban elsősorban a megvalósításra fogsz összpontosítani többszálú alkalmazásokkal PythonKét fő modul használható a szálak kezelésére a Python:
- Az szál modul, és
- Az befűzés modul
Azonban a Pythonlétezik egy úgynevezett globális interpreter lock (GIL) is. Ez nem tesz lehetővé jelentős teljesítménynövekedést, sőt akár csökkenteni egyes többszálú alkalmazások teljesítménye. Az oktatóanyag következő részeiben mindent megtudhat róla.
A Thread és Threading modulok
Az oktatóanyagban megismert két modul a menet modul és a menetvágó modul.
A szálmodul azonban már régóta elavult. Kezdve ezzel Python 3, azt elavultnak minősítették, és csak a következő néven érhető el _szál visszafelé kompatibilitás érdekében.
A magasabb szintet kell használnia befűzés modul a telepíteni kívánt alkalmazásokhoz. A szál modult itt csak oktatási célokból tárgyaltuk.
A szál modul
A szintaxis egy új szál létrehozásához ezzel a modullal a következő:
thread.start_new_thread(function_name, arguments)
Rendben, most lefedte az alapvető elméletet a kódolás megkezdéséhez. Szóval nyisd ki a te IDLE vagy egy jegyzettömböt, és írja be a következőket:
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))
Mentse el a fájlt, és nyomja meg az F5 billentyűt a program futtatásához. Ha minden helyesen történt, akkor ezt a kimenetet kell látnia:
A következő részekben többet is megtudhatsz a versenykörülményekről és azok kezeléséről.
KÓDMAGYARÁZAT
- Ezek az utasítások importálják az idő és a szál modult, amelyek a végrehajtás és a késleltetés kezelésére szolgálnak. Python szálak.
- Itt egy függvényt definiáltunk thread_test, amelyet a start_new_thread metódus. A függvény egy while ciklust futtat négy iteráción keresztül, és kinyomtatja a hívó szál nevét. Az iteráció befejezése után egy üzenetet nyomtat, amely szerint a szál végrehajtása befejeződött.
- Ez a program fő része. Itt egyszerűen hívja a start_new_thread módszerrel a thread_test függvényt argumentumként. Ez egy új szálat hoz létre az argumentumként átadott függvényhez, és elkezdi annak végrehajtását. Fontos megjegyezni, hogy ezt (thread_test) bármilyen más függvénnyel lecserélheted, amelyet szálként szeretnél futtatni.
A menetkészítő modul
Ez a modul a szálkezelés magas szintű implementációja a következőben: Python és a többszálú alkalmazások kezelésének tényleges szabványa. A szálmodulhoz képest széleskörű funkciókat kínál.
A Threading modul felépítése
Íme egy lista az ebben a modulban meghatározott hasznos funkciókról:
| Funkció neve | Leírás |
|---|---|
| activeCount() | A számot adja vissza Szál még élő tárgyak. |
| currentThread() | A Thread osztály aktuális objektumát adja vissza. |
| felsorolja () | Felsorolja az összes aktív szál objektumot. |
| isDaemon() | Igaz értéket ad vissza, ha a szál démon. |
| életben van() | Igaz értéket ad vissza, ha a szál még él. |
| Szálosztály metódusok | |
| Rajt() | Elindítja egy szál tevékenységét. Minden szálhoz csak egyszer kell meghívni, mert többszöri hívás esetén futásidejű hibát dob. |
| fuss() | Ez a metódus egy szál tevékenységét jelöli, és a Thread osztályt kiterjesztő osztállyal felülírható. |
| csatlakozni () | Addig blokkolja a többi kód végrehajtását, amíg a szál, amelyen a join() metódus meghívásra került, meg nem szakad. |
Háttértörténet: The Thread Class
Mielőtt elkezdenél többszálú programokat kódolni a szálkezelő modullal, elengedhetetlen a Thread osztály megértése. A szálkezelő osztály az elsődleges osztály, amely meghatározza a szál sablonját és műveleteit a szálban. Python.
A többszálú létrehozásának leggyakoribb módja Python Az alkalmazás lényege, hogy deklaráljunk egy osztályt, amely kiterjeszti a Thread osztályt és felülírja annak run() metódusát.
A Thread osztály összefoglalva egy különálló kódsorozatot jelöl szál az irányítás.
Tehát többszálú alkalmazás írásakor a következőket kell tennie:
- definiáljon egy osztályt, amely kiterjeszti a Thread osztályt
- Felülírja a __init__ konstruktőr
- Felülírja a fuss() módszer
Miután egy szál objektum elkészült, a Rajt() a metódus felhasználható a tevékenység végrehajtásának megkezdésére, és a csatlakozni () metódus használható az összes többi kód blokkolására az aktuális tevékenység befejezéséig.
Most próbáljuk meg a szálkezelő modult használni az előző példád megvalósításához. Indítsd el újra a IDLE és írja be a következőket:
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()
Ez lesz a kimenet, amikor végrehajtja a fenti kódot:
KÓDMAGYARÁZAT
- Ez a rész megegyezik az előző példánkkal. Itt importálod az idő és a szál modult, amelyek a végrehajtás és a késleltetések kezelésére szolgálnak. Python szálak.
- Ebben a bitben egy threadtester nevű osztályt hoz létre, amely örökli vagy kiterjeszti a Szál a szálkezelő modul osztálya. Ez az egyik leggyakoribb módja a szálak létrehozásának a PythonAzonban csak a konstruktort és a fuss() módszert az alkalmazásban. Amint a fenti kódmintán látható, a __init__ metódus (konstruktor) felül lett írva. Hasonlóképpen felülbírálta a fuss() módszer. A szálon belül végrehajtani kívánt kódot tartalmazza. Ebben a példában meghívta a thread_test() függvényt.
- Ez a thread_test() metódus, amely a következő értéket veszi fel: i argumentumként, minden iterációban 1-gyel csökkenti, és végigmegy a kód többi részén, amíg az i értéke 0 nem lesz. Minden iterációban kinyomtatja az aktuálisan futó szál nevét, és várakozási másodperceket vár (amit szintén argumentumként vesz fel).
- thread1 = threadtester(1, “First Thread”, 1) Itt létrehozunk egy szálat, és átadjuk a három paramétert, amelyeket az __init__-ban deklaráltunk. 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 ciklus hányszor futjon le.
- thread2.start() A start metódus egy szál végrehajtásának elindítására szolgál. Belsőleg a start() függvény meghívja az osztály run() metódusát.
- 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 tudjuk, az ugyanabban a folyamatban lévő szálak hozzáférnek az adott folyamat memóriájához és adataihoz. Ennek eredményeként, ha egynél több szál próbálja meg egyszerre módosítani vagy elérni az adatokat, hibák léphetnek fel.
A következő részben bemutatjuk a különböző típusú komplikációkat, amelyek akkor jelentkezhetnek, amikor a szálak a meglévő hozzáférési tranzakciók ellenőrzése nélkül férnek hozzá az adatokhoz és a kritikus szakaszhoz.
Holtpontok és versenyfeltételek
Mielőtt belekezdenénk a holtpontok és a versenyfeltételek megismerésébe, hasznos lesz megérteni néhány alapvető definíciót a párhuzamos programozással kapcsolatban:
- Kritikus szakasz: Ez egy kódrészlet, amely megosztott változókhoz fér hozzá vagy módosítja azokat, és atomi tranzakcióként kell végrehajtani.
- Kontextusváltás: Ez az a folyamat, amelyet a CPU követ egy szál állapotának tárolására, mielőtt egyik feladatról a másikra váltana, hogy később ugyanonnan folytatható legyen.
Holtpontok
Holtpontok a legfélelmetesebb problémák, amelyekkel a fejlesztők szembesülnek, amikor párhuzamos/többszálú alkalmazásokat írnak PythonA holtpontok megértésének legjobb módja a klasszikus számítástechnikai példafeladat, az úgynevezett Étkezés Philosophers Probléma.
Az étkezési filozófusok problémafelvetése a következő:
Öt filozófus ül egy kerek asztalnál, öt tányér spagettivel (egyfajta tésztával) és öt villával, ahogy az ábrán látható.
Étkezés Philosophers Probléma
Egy filozófusnak bármikor eszik, vagy gondolkodik.
Sőt, a 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, ha mind az öt filozófus egyszerre veszi fel a jobb villát.
Mivel mindegyik filozófusnak van egy villája, mindannyian megvárják, míg a többiek leteszik a villát. Ennek eredményeként egyikük sem fog tudni enni spagettit.
Hasonlóképpen, egy párhuzamos rendszerben patthelyzet lép fel, amikor különböző szálak vagy folyamatok (filozófusok) egyszerre próbálják megszerezni a megosztott rendszererőforrásokat (forkokat). Ennek eredményeként egyik folyamat sem kap lehetőséget a végrehajtásra, mivel egy másik folyamat által birtokolt másik erőforrásra vár.
A verseny feltételei
A versenyhelyzet egy program nemkívánatos állapota, amely akkor következik be, amikor egy rendszer két vagy több műveletet hajt végre egyszerre. Vegyük például ezt az egyszerű for ciklust:
i=0; # a global variable for x in range(100): print(i) i+=1;
Ha létrehoz n Ha egyszerre több szál futtatja ezt a kódot, nem lehet meghatározni az i értékét (amelyet a szálak megosztanak), amikor a program befejezi a végrehajtást. Ez azért van, mert egy valódi többszálú környezetben a szálak átfedhetik egymást, és az i értéke, amelyet egy szál lekérett és módosított, megváltozhat, amikor egy másik szál hozzáfér hozzá.
Ez a két fő problématípus, amelyek többszálú vagy elosztott rendszerekben előfordulhatnak: Python alkalmazás. A következő részben megtudhatja, hogyan lehet ezt a problémát a szálak szinkronizálásával megoldani.
Synchronizáló szálak
A versenykörülmények, a holtpontok és más, szálon alapuló problémák kezelésére a menetkészítő modul biztosítja a Zár objektum. Az ötlet az, hogy amikor egy szál hozzá akar férni egy adott erőforráshoz, akkor zárolást szerez az adott erőforrás számára. Ha egy szál zárol egy adott erőforrást, más szál sem férhet hozzá addig, amíg a zárolást fel nem oldja. Ennek eredményeképpen az erőforrásban végbemenő változások lesznek, a versenykörülmények pedig elkerülhetők lesznek.
A zár egy alacsony szintű szinkronizálási primitív, amelyet a _szál modul. Egy adott időpontban egy zár két állapotban lehet: zárt or feloldva. Két módszert támogat:
- szerez(): Amikor a zárolt állapot feloldott, az acquire() metódus meghívása zárolt állapotra változtatja az állapotot és visszatérési értéket ad. Ha azonban az állapot zárolt, az acquire() metódus hívása blokkolva van, amíg a release() metódust egy másik szál meg nem hívja.
- kiadás(): A release() metódus arra szolgál, hogy az állapotot feloldottra állítsa, azaz feloldja a zárolást. Bármelyik szál hívhatja, nem feltétlenül az, amelyik megszerezte a zárat.
Íme egy példa a zárak használatára az alkalmazásaidban. Indítsd el a IDLE és írja be a következőt:
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()
Most nyomja meg az F5 billentyűt. Ilyen kimenetet kell látnod:
KÓDMAGYARÁZAT
- Itt egyszerűen létrehoz egy új zárat a következő hívásával befűzés.Lock() gyári funkció. Belsőleg a Lock() a platform által karbantartott leghatékonyabb konkrét Lock osztály egy példányát adja vissza.
- Az első utasításban a zárolást az megszerez() metódus meghívásával szerezheti meg. Ha a zárolást engedélyezte, nyomtat „zár megszerzett” a konzolhoz. Miután a szál futtatni kívánt összes kódja befejeződött, feloldja a zárolást a release() metódus meghívásával.
Az elmélet rendben van, de honnan tudhatod, hogy a zárolás valóban működött? Ha megnézed a kimenetet, látni fogod, hogy mindegyik print utasítás pontosan egy sort nyomtat ki egyszerre. Emlékezzünk vissza, hogy egy korábbi példában a print kimenetei véletlenszerűek voltak, mivel több szál is hozzáfért a print() metódushoz egyszerre. Itt a print függvényt csak a zárolás megszerzése után hívjuk meg. Tehát a kimenetek egyenként, soronként jelennek meg.
A zárakon kívül, Python emellett néhány más mechanizmust is támogat a szálak szinkronizálásának kezelésére, az alábbiak szerint:
- RLocks
- Semaphores
- Feltételek
- Események, ill
- Akadályok
Global Interpreter Lock (és hogyan kell kezelni)
Mielőtt belemennénk a részletekbe PythonGIL-lel kapcsolatban, definiáljunk néhány kifejezést, amelyek hasznosak lesznek a következő szakasz megértéséhez:
- CPU-hoz kötött kód: ez minden olyan kódrészletre vonatkozik, amelyet közvetlenül a CPU hajt végre.
- I/O-kötésű kód: ez bármilyen kód lehet, amely az operációs rendszeren keresztül fér hozzá a fájlrendszerhez.
- CPython: ez a hivatkozás végrehajtás of Python és a C-ben írt interpreterként írható le és Python (programozási nyelv).
Miben található a GIL Python?
Globális tolmácszár (GIL) in Python A GIL egy folyamatzár vagy mutex, amelyet a folyamatok kezelésekor használnak. Biztosítja, hogy egy szál egyszerre csak egy adott erőforráshoz férhessen hozzá, és megakadályozza az objektumok és bájtkódok egyidejű használatát is. Ez az egyszálú programok teljesítménynövekedésével jár. GIL a következőben: Python nagyon egyszerű és könnyen megvalósítható.
A zár segítségével meg lehet győződni arról, hogy egy adott időpontban csak egy szál férhet hozzá egy adott erőforráshoz.
Az egyik jellemzője Python az, hogy globális zárolást használ minden értelmező folyamaton, ami azt jelenti, hogy minden folyamat a Python maga az értelmező, mint erőforrás.
Például tegyük fel, hogy írtál egy Python egy olyan program, amely két szálat használ a CPU és az 'I/O' műveletek végrehajtásához. A program futtatásakor a következő történik:
- Az Python Az interpreter létrehoz egy új folyamatot és elindítja a szálakat.
- Amikor a szál-1 elindul, először beszerzi a GIL-t és zárolja.
- Ha a 2. szál most végre akar hajtani, akkor meg kell várnia a GIL kiadását, még akkor is, ha egy másik processzor szabad.
- Tegyük fel, hogy az 1. szál I/O műveletre vár. Ekkor kiadja a GIL-t, és a thread-2 megszerzi azt.
- Az I/O műveletek befejezése után, ha az 1-es szál most végre akar hajtani, ismét meg kell várnia, amíg a GIL-t felszabadítja a 2. szál.
Emiatt egyszerre csak egy szál férhet hozzá az értelmezőhöz, ami azt jelenti, hogy csak egy szál fog futni. Python kód egy adott időpontban.
Ez egymagos processzorokban rendben van, mivel időszeletelést használna (lásd a bemutató első részét) a szálak kezelésére. Többmagos processzorok esetén azonban egy CPU-hoz kötött, több szálon futó függvény jelentős hatással lesz a program hatékonyságára, mivel valójában nem fogja egyszerre az összes elérhető magot használni.
Miért volt szükség a GIL-re?
a CPython A szemétgyűjtő hatékony memóriakezelési technikát, az úgynevezett referenciaszámlálást használ. Így működik: Minden objektum a Python rendelkezik egy referenciaszámmal, amely növekszik, ha új változónévhez rendelik, vagy konténerhez adják (például tuple-khez, listákhoz stb.). Hasonlóképpen, a referenciaszám csökken, ha a hivatkozás kilép a hatókörből, vagy ha a del utasítást meghívják. Amikor egy objektum referenciaszáma eléri a nullát, a rendszer szemetet gyűjt, és a lefoglalt memória felszabadul.
A probléma azonban az, hogy a referenciaszám változója, mint bármely más globális változó, hajlamos a versenyfeltételekre. A probléma megoldása érdekében a fejlesztők Python úgy döntött, hogy a globális interpreter lockot használja. A másik lehetőség az volt, hogy minden objektumhoz hozzáadtak egy zárat, ami holtpontokat és az acquire() és release() hívások miatti megnövekedett terhelést eredményezett volna.
Ezért a GIL jelentős korlátozás a többszálú folyamatok számára. Python nagy CPU-igényű műveleteket futtató programok (ami gyakorlatilag egyszálúvá teszi őket). Ha több CPU-magot szeretne használni az alkalmazásában, használja a több feldolgozás modul helyett.








