Többszálú bemenet Python példával: Tanulja meg a GIL-t Python

A python programozási nyelv lehetővé teszi a multiprocessing vagy a multithreading használatát. Ebből az oktatóanyagból megtudhatja, hogyan írhat többszálú alkalmazásokat Python.

Mi az a szál?

A szál a párhuzamos programozás végrehajtási egysége. A 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 egyenként is végrehajthatók, miközben megosztják a folyamaterőforrásaikat.

Mi az a folyamat?

A folyamat alapvetően a futó 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ét a fő szálal, ami megkönnyíti és hatékonyan teszi az információmegosztást és a szálakon belüli kommunikációt. A szálak könnyebbek, mint a folyamatok. A több szál egyenként is végrehajtható, miközben megosztja a folyamat erőforrásait. A többszálú feldolgozás célja több feladat és függvénycella 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 vegye figyelembe a következő forgatókönyvet: A számítógépen lévő .exe fájl egy program. Amikor megnyitja, az operációs rendszer betölti a memóriába, és a CPU végrehajtja. A program éppen futó példányát folyamatnak nevezzük.

Minden folyamatnak 2 alapvető összetevője lesz:

  • A kód
  • 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álra úgy gondolhat, mint a folyamat egy szakaszára, amelyet az operációs rendszer külön is végrehajthat.

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

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 MultiThreading

Python támogatja a konstrukciókat mind a többfeldolgozáshoz, mind a többszálú feldolgozáshoz. Ebben az oktatóanyagban elsősorban a megvalósításra fog összpontosítani többszálú alkalmazások python segítségével. Két fő modul használható a szálak kezelésére Python:

  1. A szál modul, és
  2. A befűzés modul

A pythonban azonban létezik valami, amit globális értelmező zárnak (GIL) neveznek. Nem tesz lehetővé nagy teljesítménynövekedést, sőt még az is lehet 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 __cérna 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élbó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 szál modul

A következő részekben többet megtudhat a verseny körülményeiről és azok kezeléséről

A szál modul

KÓDMAGYARÁZAT

  1. Ezek az utasítások importálják az idő és szál modult, amelyek a végrehajtás és a késleltetés kezelésére szolgálnak Python szálak.
  2. Itt egy függvényt definiáltunk thread_test, amelyet a start_new_thread módszer. A függvény egy while ciklust futtat négy iterációig, és kiírja az őt 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.
  3. Ez a program fő része. Itt egyszerűen hívja a start_new_thread módszerrel a thread_test Ez egy új szálat hoz létre az argumentumként átadott függvényhez, és elkezdi végrehajtani. Vegye figyelembe, hogy ezt lecserélheti (szál_test) bármely más, szálként futtatni kívánt funkcióval.

A menetkészítő modul

Ez a modul a szálfűzés magas szintű megvalósítása a pythonban, és a többszálú alkalmazások kezelésének de facto szabványa. A menetmodulhoz képest a funkciók széles skáláját kínálja.

A Threading modul felépítése
A Threading modul felépítése

Íme egy lista az ebben a modulban meghatározott hasznos funkciókról:

Funkció neve Description
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é a többszálú programok kódolását a szálfűző modul használatával, létfontosságú, hogy megértse a Thread osztályt. A szál osztály az elsődleges osztály, amely meghatározza a sablont és a szál műveleteit a pythonban.

A többszálú Python-alkalmazás létrehozásának legáltalánosabb módja egy olyan osztály deklarálása, amely kiterjeszti a Thread osztályt, és felülírja a 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:

  1. definiáljon egy osztályt, amely kiterjeszti a Thread osztályt
  2. Felülírja a __init__ konstruktőr
  3. Felülírja a fuss() módszer

Miután egy szál objektum elkészült, a Rajt() módszerrel lehet megkezdeni ennek a tevékenységnek a végrehajtását é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álfűző modult használni az előző példa megvalósításához. Még egyszer, gyújtsd fel 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:

Háttértörténet: The Thread Class

KÓDMAGYARÁZAT

Háttértörténet: The Thread Class

  1. Ez a rész megegyezik az előző példánkkal. Itt importálhatja az idő és szál modult, amely a végrehajtás és a késleltetések kezelésére szolgál Python szálak.
  2. Ebben a bitben egy threadtester nevű osztályt hoz létre, amely örökli vagy kiterjeszti a Szál a menetvágó modul osztálya. Ez az egyik leggyakoribb módja a szálak létrehozásának a pythonban. Azonban 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.
  3. Ez a thread_test() metódus, amely a következő értékét veszi fel i argumentumként minden iterációnál 1-gyel csökkenti, és a kód többi részében körözik, amíg i nem lesz 0. Minden iterációban kiírja az éppen futó szál nevét, és várakozási másodpercig alszik (ami szintén argumentumnak számít ).
  4. 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.
  5. thread2.start()T 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ályod 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 tudja, 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 próbálja meg egyszerre módosítani vagy elérni az adatokat, hibák léphetnek fel.

A következő részben látni fogja azokat a különféle bonyodalmakat, amelyek akkor jelentkezhetnek, ha a szálak hozzáférnek az adatokhoz és a kritikus szakaszokhoz 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 lesz megérteni néhány alapvető definíciót a párhuzamos programozással kapcsolatban:

  • Critical SectionEz egy kódrészlet, amely eléri vagy módosítja a megosztott változókat, és atomi tranzakcióként kell végrehajtani.
  • Context SwitchEz 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, így később ugyanattól a ponttól folytatható.

Holtpontok

Holtpontok ezek a legfélelmetesebb probléma, amellyel a fejlesztők szembesülnek, amikor párhuzamos/többszálas alkalmazásokat írnak pythonban. A holtpontok megértésének legjobb módja a klasszikus számítástechnikai példaprobléma, a Étkezés Philosophers Probléma.

Az étkezési filozófusok problémafelvetése a következő:

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

Étkezés Philosophers Probléma

É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 versenyfeltétel egy program nem kívánt állapota, amely akkor következik be, amikor egy rendszer egyidejűleg két vagy több műveletet hajt végre. Vegyük például ezt az egyszerű ciklust:

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

Ha létrehoz n A kódot egyszerre futtató szálak száma nem tudja meghatározni az i értékét (amelyet a szálak osztanak meg), amikor a program befejezi a végrehajtást. Ennek az az oka, hogy egy valódi többszálú környezetben a szálak átfedhetik egymást, és a szál által leolvasott és módosított i értéke időközben megváltozhat, amikor egy másik szál hozzáfér.

Ez a problémák két fő osztálya, amelyek többszálú vagy elosztott python alkalmazásokban fordulhatnak elő. A következő részben megtudhatja, hogyan oldhatja meg ezt a problémát a szálak szinkronizálásával.

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 __cérna modult. A zár bármikor a következő két állapot egyikében lehet: zárt or feloldva. Két módszert támogat:

  1. szerez()Amikor a lock-state fel van oldva, az hanki() metódus meghívása zárolt állapotba állítja, és visszatér. Ha azonban az állapot zárolva van, akkor az hanki() hívása blokkolva lesz, amíg a release() metódust meg nem hívja egy másik szál.
  2. 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ásokban. Gyújtsa be a sajátját 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:

Synchronizáló szálak

KÓDMAGYARÁZAT

Synchronizáló szálak

  1. 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.
  2. 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 tudod, hogy a zár valóban működött? Ha megnézi a kimenetet, látni fogja, hogy a print utasítások mindegyike pontosan egy sort nyomtat egyszerre. Emlékezzünk vissza, hogy egy korábbi példában a print kimenetei véletlenszerűek voltak, mivel egyszerre több szál is elérte a print() metódust. Itt a nyomtatási funkció csak a zárolás után kerül meghívásra. Tehát a kimenetek egyenként és soronként jelennek meg.

A zárakon kívül a python néhány más mechanizmust is támogat a szálszinkronizálás kezelésére, az alábbiak szerint:

  1. RLocks
  2. Semaphores
  3. Körülmények
  4. Események, ill
  5. Akadályok

Global Interpreter Lock (és hogyan kell kezelni)

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

  1. CPU-hoz kötött kód: ez bármely olyan kódrészletre 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 éri el a fájlrendszert
  3. 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) a pythonban egy folyamatzár vagy egy mutex, amelyet a folyamatok kezelésére használnak. Gondoskodik arról, hogy egy szál egyszerre férhessen hozzá egy adott erőforráshoz, és megakadályozza az objektumok és bájtkódok egyidejű használatát. Ez előnyt jelent az egyszálú programok számára a teljesítménynövekedésben. A GIL a pythonban 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 magát a python interpretert erőforrásként kezeli.

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

  1. A python interpreter új folyamatot hoz létre, és létrehozza a szálakat
  2. Amikor a szál-1 elindul, először beszerzi a GIL-t és zárolja.
  3. 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.
  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 azt.
  5. 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, vagyis egy adott időpontban csak egy szál hajtja végre a python kódot.

Ez rendben van egy egymagos processzorban, mert időszeletelést használna (lásd az oktatóanyag első részét) a szálak kezelésére. A többmagos processzorok esetében azonban a több szálon végrehajtott, CPU-hoz kötött függvény 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 a GIL-re?

a CPython A szemétgyűjtő hatékony memóriakezelési technikát használ, amelyet referenciaszámlálásnak neveznek. Így működik: A pythonban minden objektumnak van hivatkozási száma, amely megnövekszik, ha új változónévhez rendelik, vagy hozzáadják egy tárolóhoz (például sorokhoz, listákhoz stb.). Hasonlóképpen a hivatkozások száma csökken, ha a hivatkozás kikerül a hatókörből, vagy amikor a del utasítás meghívásra kerül. Amikor egy objektum referenciaszáma eléri a 0-t, akkor az összegyűjti a szemetet, és felszabadítja a lefoglalt memóriát.

De a probléma az, hogy a referenciaszám-változó hajlamos a versenyfeltételekre, mint bármely más globális változó. A probléma megoldására a python fejlesztői a globális tolmácszár használata mellett döntöttek. A másik lehetőség az volt, hogy minden objektumhoz zárolást adtunk, ami holtpontokhoz és megnövelt többletköltséghez vezetett volna az discover() és release() 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 (effektíve egyszálassá teszik őket). Ha több CPU magot szeretne használni az alkalmazásban, használja a több feldolgozás modul helyett.

Összegzésként

  • Python 2 modult támogat a többszálú feldolgozáshoz:
    1. __cérna modul: Alacsony szintű megvalósítást biztosít a szálfűzéshez, és elavult.
    2. menetvágó modul: Magas szintű megvalósítást biztosít a többszálú feldolgozáshoz, és ez a jelenlegi szabvány.
  • Ha szálat szeretne létrehozni a szálfűző modul segítségével, a következőket kell tennie:
    1. Hozzon létre egy osztályt, amely kiterjeszti a Szál osztály.
    2. Felülírja a konstruktorát (__init__).
    3. Felülírni fuss() módszer.
    4. Hozzon létre egy objektumot ebből az osztályból.
  • Egy szál a következő meghívásával hajtható végre Rajt() módszer.
  • A csatlakozni () metódus használható más szálak blokkolására mindaddig, amíg ez a szál (az, amelyiken a csatlakozást hívták) be nem fejezi a végrehajtást.
  • Versenyfeltétel akkor fordul elő, ha több szál egyszerre ér el vagy módosít egy megosztott erőforrást.
  • Ezzel elkerülhető Synchronizáló szálak.
  • Python 6 módot támogat a szálak szinkronizálására:
    1. Zárak
    2. RLocks
    3. Semaphores
    4. Körülmények
    5. Események, ill
    6. Akadályok
  • A zárak csak egy adott szálnak engedik belépni a kritikus szakaszba, amelyik megszerezte a zárolást.
  • A zárnak 2 elsődleges módja van:
    1. szerez(): Beállítja a zárolási állapotot zárt. Ha egy zárolt objektumra hívják, akkor blokkolja, amíg az erőforrás fel nem szabadul.
    2. kiadás(): Beállítja a zárolási állapotot zárolt és visszatér. Ha nem zárolt objektumra hívják, akkor false értéket ad vissza.
  • A globális tolmácszár olyan mechanizmus, amelyen keresztül csak 1 CPython tolmács folyamat végrehajtható egy időben.
  • A C referencia számláló funkciójának megkönnyítésére használtákPythons szemétszedője.
  • Ahhoz, hogy Python nehéz CPU-hoz kötött műveletekkel rendelkező alkalmazások esetén használja a többfeldolgozó modult.