Python Concurrency Tutorial

Ez a PyCon 2020 bemutatóm összefoglalója. Az eredeti videót és a forráskódot itt találod: https://github.com/santiagobasulto/pycon-concurrency-tutorial-2020

https://www.youtube.com/watch?v=18B1pznaU1o

Ez egy gyors útmutató/tutorial arról, hogyan lehet hatékonyan párhuzamos programokat írni Python segítségével. Az egyidejűség a Pythonban zavaró lehet. Több modul is létezik (threading, _thread, multiprocessing, subprocess). Van a sokat gyűlölt GIL is, de csak a CPythonhoz (a PyPy és a Jython nem rendelkezik GIL-lel). A gyűjtemények nem szálbiztosak, kivéve néhány megvalósítási részletet a CPython esetében.

Ez a bemutató célja, hogy földhözragadt ajánlásokat adjon arra vonatkozóan, hogyan közelítsük meg a párhuzamos (vagy párhuzamos) kód egyes felhasználási eseteit.

Megfigyelheti, hogy néhány más, hagyományosabb, “csak többszálas” Python bemutatóval ellentétben én mind a számítógép-architektúra, mind az operációs rendszerek fogalmainak összefoglalásával kezdem. Ezeknek az alapfogalmaknak a megértése alapvető fontosságú az egyidejű programok helyes felépítéséhez. Kérjük, hagyja ki ezeket a részeket, ha már ismeri ezeket a témákat.

Ez az oktatóanyag a következő részekre oszlik:

  • Párhuzamosság vs. párhuzamosság
  • Számítógép architektúra
  • Az operációs rendszer szerepe
  • Szálak Pythonnal
  • Szálak szinkronizálása
  • Multiprocesszálás
  • Magas szintű könyvtárak: concurrent.futures és parallel

Párhuzamosság vs. párhuzamosság

Párhuzamosság az, amikor több feladat fut egyszerre. Ez a párhuzamos programok végső célja. Az egyidejűség kevesebb, mint a párhuzamosság, ez azt jelenti, hogy több feladatot indítunk és zsonglőrködünk velük ugyanabban az időben. Egy adott pillanatban azonban egyszerre csak egyet csinálunk.

A főzésre alkalmazva ezek lennének példák az egyidejűségre és a párhuzamosságra.

A párhuzamosságban csak egy ember van a konyhában:

  • Kezdjük el a hagymavágást
  • Kezdjük el a serpenyő melegítését
  • Finish cutting the onions

Ez esetben egyértelmű, hogy nem tudunk több dolgot EGYIDŐBEN csinálni. Elkezdhetjük mindegyiket, de ide-oda kell ugrálnunk, hogy irányítsuk őket.

Párhuzamosan több ember van a konyhában:

  • Személy 1 hagymát vág
  • Személy 2 paprikát vág
  • Személy 3 várja, hogy a serpenyő felmelegedjen

Ebben az esetben egyszerre több feladatot végeznek.

Számítógép-architektúra

Minden modern számítógép egyszerűsíthető a von Neumann-architektúra segítségével, amelynek 3 fő összetevője van: Számítás (CPU), Memória (RAM), I/O (merevlemezek, hálózatok, videokimenet).

A von Neumann-architektúra

A kódunk mindig az egyes komponensekhez kapcsolódó feladatokat fog végrehajtani. Néhány utasítás “számítási” ( x + 1), néhány memória (x = 1), és néhány I/O (fp = open('data.csv')).

Hogy megfelelően megértsük az egyidejűség részleteit Pythonban, fontos szem előtt tartani ezen komponensek különböző “hozzáférési idejét”. A CPU elérése sokkal gyorsabb, mint a RAM elérése, nem is beszélve az I/O-ról. Van egy tanulmány, amely a számítógépes késleltetést az emberi relatív időkkel hasonlítja össze: ha egy CPU-ciklus 1 másodperc emberi időt jelent, akkor egy hálózati kérés San Franciscóból Hongkongba 11 évig tart.

Késleltetés emberi léptékben (forrás)

A radikális időbeli különbség ezekkel az “utasításokkal” alapvető lesz a szálak és a Python GIL megvitatása szempontjából.

Az operációs rendszer szerepe

Ha egy egyetemi kurzust építenék a párhuzamosságról, egy egész előadást csak az operációs rendszerek történetéről tartanék. Csak így érthetjük meg, hogyan fejlődtek az egyidejűség mechanizmusai.

Az első fogyasztói célú operációs rendszerek (gondoljunk csak az MS-DOS-ra) nem támogatták a többfeladatos működést. Legfeljebb csak egy program futott egy adott időpontban. Ezeket követték a “kooperatív többfeladatos” rendszerek (Windows 3.0). Ez a fajta multitasking inkább hasonlított egy “eseményhurokra” (gondoljunk a Python asynciójára), mint egy modern többszálú programra. A kooperatív multitasking operációs rendszerben az alkalmazás feladata a CPU “felszabadítása”, amikor már nincs rá szükség, és hagyja, hogy más alkalmazások fussanak. Ez nyilvánvalóan nem működött, mivel nem bízhatsz minden alkalmazásban (vagy akár a fejlesztőidben sem, hogy elég éles eszűek ahhoz, hogy helyesen döntsék el, mikor kell felszabadítani a CPU-t). A modern operációs rendszerek (Windows 95, Linux stb.) a multitasking kialakítását “Preemptive Multitasking”-ra állították át, amelyben az operációs rendszer dönti el, hogy mely alkalmazások kapják meg a CPU-t, milyen gyakran és mennyi ideig.

Az operációs rendszer dönti el tehát, hogy mely folyamatok futhatnak, és mennyi ideig. Ez teszi lehetővé, hogy az alkalmazás akkor is “többfeladatosnak” érezze magát, ha csak 1 CPU-val rendelkezik. Az operációs rendszer ütemezője nagyon gyorsan váltogatja a folyamatokat ide-oda.

A következő ábrán 3 folyamat (P1, P2, P3) fut párhuzamosan, ugyanazon a CPU-n osztozva (a kék vonal a CPU-idő). A P1 elég sokáig fut, mielőtt gyorsan átvált P2-re, majd P3-ra, és így tovább.

3 folyamat fut “egyidejűleg”

A folyamat

A folyamat az az absztrakció, amelyet az operációs rendszer a kód futtatására használ. A kódodat ebbe a folyamatba csomagolja, és memóriát és más megosztott erőforrásokat rendel hozzá. A processz absztrakció lehetővé teszi az operációs rendszer számára, hogy “megkülönböztesse” a futó programokat, így azok konfliktusokat okoznak egymással. Például az 1. folyamat nem férhet hozzá a 2. folyamat által lefoglalt memóriához. Ez a felhasználók biztonsága szempontjából is fontos. Ha az 1. felhasználó elindít egy folyamatot, akkor ez a folyamat képes lesz olvasni az általa elérhető fájlokat.

A következő ábra a Process vizuális ábrázolását tartalmazza. A processz tartalmazza a végrehajtandó kódot, a kiosztott RAM-ot és a program által létrehozott összes adatot (változók, megnyitott fájlok stb.).

Egy OS Process vizuális ábrázolása

Most láthatjuk, hogy ugyanaz a “program” (ugyanaz a kódrészlet) több folyamatban fut egyidejűleg.

Most áttérhetünk a második fázisra: hogyan valósul meg valójában az egyidejűség (vagy akár a párhuzamosság)? Fejlesztői szemszögből nézve kétféle mechanizmust alkalmazhatunk az “egyidejűség” elérésére: a többszálú futást vagy a többprocesszoros futást.

Multiszálú futás

A futamok egyazon folyamaton belüli párhuzamos végrehajtási folyamatok. A folyamat több végrehajtási szálat indít, amelyek egymástól függetlenül “fejlődnek” aszerint, hogy mi a feladatuk.

Tegyük fel például, hogy van egy programunk, amelynek három weboldalról kell adatokat letöltenie, majd egy végső jelentéshez egyesítenie. Minden weboldalnak körülbelül két másodpercig tart, amíg válaszol az adatokkal. Az egy szálon futó program valami ilyesmi lenne:

d1 = get_website_1() # 2 secs
d2 = get_website_2() # 2 secs
d3 = get_website_3() # 2 secscombine(d1, d2, d3) # very fast

A teljes végrehajtási idő >6 másodperc lesz:

Egyszálú program

Egy többszálú program egyszerre kezdi el az adatok letöltését a három weboldalról, és a végső jelentés elkészítésével megvárja, amíg mind elkészül.

t1 = Thread(get_website_1)
t2 = Thread(get_website_2)
t3 = Thread(get_website_3)wait_for_threads()d1 = t1.result()
d2 = t2.result()
d3 = t3.result()combine(d1, d2, d3)

A végrehajtás most közel ~2 másodperc lesz, mivel a három weboldal nagyjából egyszerre fog válaszolni:

Egy többszálú program

A Pythonban a szálakat a Thread osztály segítségével hozzuk létre, a threading modulból, és több példát is láthatunk az első notebookban a bemutatóm repójából: 1. Thread Basics.ipynb.

Threads and Shared Data

A szálakat kifejezetten a programod hozza létre, tehát egy folyamaton belül élnek. Ez azt jelenti, hogy minden adatot megosztanak a folyamatból. Olvashatják és módosíthatják a helyi változókat, és hozzáférhetnek ugyanahhoz a memóriához vagy ugyanazokhoz az erőforrásokhoz (fájlok, hálózati aljzatok stb.).

A futamok megosztják a folyamat adatait

Ez okozhat néhány problémát, különösen a megosztott adatok váratlan mutációját és Race Conditions-et. A 2. jegyzetfüzetben egy versenyfeltételre láthat példát. Száladatok & Versenyfeltételek.ipynb. Ugyanebben a jegyzetfüzetben bemutatunk néhány mechanizmust a szálak “szinkronizálására” a Race Conditions elkerülése érdekében, de ugyanezek a mechanizmusok potenciálisan bevezethetik a Deadlock problémáját. A jegyzetfüzetben van egy példa a Deadlock-ra: 3. Deadlocks.ipynb.

Producerek, fogyasztók és várólisták

A szinkronizációs problémák egyszerűen a paradigma megváltoztatásával is megoldhatók.

A szinkronizációs problémák elkerülésének legjobb módja a szinkronizáció teljes elkerülése.

Ez a Producer-Consumer minta használatával érhető el. A termelő szálak “függő feladatokat” hoznak létre, amelyeket végre kell hajtaniuk, majd egy közös várólistára helyezik őket. A fogyasztói szálak olvassák a feladatokat a várólistáról, és elvégzik a tényleges munkát.

Pythonban a queue modul Queue osztálya egy szálbiztos várólista, amelyet ilyen célokra használnak.

A
4. füzetben van egy példa a Producer-Consumer modellre. Producer-Consumer modell.ipynb.

A Python Global Interpreter Lock

A GIL a Python egyik leggyűlöltebb tulajdonsága. Valójában nem is olyan rossz, de szörnyű a híre.

A GIL egy “globális” zár, amelyet a Python-értelmező helyez el, és amely garantálja, hogy egy adott időben csak 1 szál férhet hozzá a CPU-hoz. Ez valóban hülyén hangzik: miért aggódsz a többszálú kód miatt, ha úgyis csak egy szál fog tudni hozzáférni a CPU-hoz egy adott időben?

Itt fontos megérteni, hogy a kódod nem csak a CPU használatáról szól. A kódod különböző feladatokat fog végezni, mint például fájlok olvasása, információszerzés a hálózatról, stb. Emlékszel a késleltetett elérési időkre? Itt vannak újra:

Késleltetés emberi léptékben (forrás)

Ez a kulcs a GIL korlátozása elleni küzdelemben. Abban a pillanatban, amikor egy szálnak valamit be kell olvasnia egy fájlból, felszabadíthatja a CPU-t, és hagyhatja más szálakat futni. Sok időbe telik, amíg a merevlemez visszaadja az adatot. ez azért van, mert a legtöbb program úgynevezett I/O-kötött. Ez azt jelenti, hogy sok I/O funkciót használnak (hálózat, fájlrendszer stb.).

Egy I/O-kötött feladat “bizonyítékát” láthatod a jegyzetfüzetben: 5. A Python GIL.ipynb.

Multiprocessing

Néhány más program elkerülhetetlenül CPU-kötött, például olyan programok, amelyeknek sok számítást kell végezniük. Néha szeretnénk felgyorsítani a dolgokat. Ha a CPU-kötött feladatokhoz többszálú futást használsz, rá fogsz jönni, hogy a GIL miatt a programjaid nem lesznek gyorsabbak (sőt, még lassabbak is lehetnek).

Elérkeztünk a multiprocessinghez, a második mechanizmushoz, amellyel párhuzamos programokat írhatunk. Ahelyett, hogy több szálunk lenne, valójában több gyermekfolyamatot fogunk létrehozni, amelyek különböző feladatok egyidejű elvégzéséről gondoskodnak.

A 6. füzetben van egy példa egy multiprocessing alapú programra. Multiprocessing.ipynb

concurrent.futures

A bemutatóm befejezéseként szeretném felhívni a figyelmet arra, hogy van egy magasabb szintű modul, amely a Python Standard Library része, és amelyet lehetőség szerint használni kell: concurrent.futures.

Ez magas szintű absztrakciókat biztosít szál- és folyamatpoolok létrehozásához, és elég érett ahhoz, hogy amikor csak lehet, alapértelmezett alternatívának tekintsük az egyidejű kód írásához.

Ezzel megvan! Szólj, ha bármilyen kérdésed van. Szívesen válaszolok rájuk.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.