Többszálas Python: csúszik az I / O szűk keresztmetszetén?

A Python párhuzamosságának kihasználása hogyan teheti gyorsabbá a szoftver nagyságrendjét.

Nemrégiben kifejlesztettem egy projektet, amelyet Hydrának hívtam: egy Pythonban írt, többszálú link-ellenőrző. Ellentétben sok Python webhely-feltérképezővel, amelyet kutatásom során találtam, a Hydra csak szabványos könyvtárakat használ, külső függőségek nélkül, mint a BeautifulSoup. CI / CD folyamat részeként kívánják futtatni, így sikereinek része volt a gyorsaság.

A Python több szála kissé megharapott téma (nem sajnálom), mivel a Python tolmács valójában nem hagyja, hogy több szál egyszerre fusson.

A Python globális tolmácszárja (GIL) megakadályozza, hogy több szál egyszerre futtassa a Python bájtkódokat. Minden futtatni kívánt szálnak először meg kell várnia, hogy a GIL felszabaduljon az éppen futó szálból. A GIL nagyjából a mikrofon egy alacsony költségvetésű konferenciapanelen, kivéve, ahol senki sem kiabálhat.

Ennek az az előnye, hogy megakadályozza a versenykörülményeket. Hiányzik azonban a több feladat párhuzamos futtatása által nyújtott teljesítménybeli előnyök. (Ha frissítést szeretne kapni a párhuzamosságról, a párhuzamosságról és a többszálú szálakról, olvassa el a Párhuzamosság, párhuzamosság és a Mikulás sok szála című részt.)

Míg én a Go-t részesítem előnyben a kényelmes első osztályú primitívek mellett, amelyek támogatják az egyidejűséget (lásd Goroutines), ennek a projektnek a címzettjei jobban érezték magukat a Pythonban. Kipróbálás és felfedezés lehetőségeként használtam!

Több feladat egyidejű végrehajtása a Pythonban nem lehetetlen; csak egy kis extra munka kell hozzá. A Hydra számára a fő előny a bemeneti / kimeneti (I / O) szűk keresztmetszet leküzdése.

A weboldalak ellenőrzéséhez Hydrának ki kell mennie az internetre, és be kell szereznie azokat. A csak a CPU által végrehajtott feladatokhoz képest a hálózaton keresztül történő kijutás viszonylag lassabb. Mennyire lassú?

Íme a tipikus számítógépen végrehajtott feladatok hozzávetőleges időzítése:

Feladat Idő
CPU tipikus utasítás végrehajtása 1/1 000 000 000 mp = 1 nanoszekundum
CPU beolvasás az L1 gyorsítótárból 0,5 nanoszekundum
CPU ág téves megjóslás 5 nanoszekundum
CPU beolvasás az L2 gyorsítótárból 7 nanosec
RAM Mutex zár / feloldás 25 nanosec
RAM beolvasás a fő memóriából 100 nanosec
Hálózat küldjön 2K bájtot 1Gbps hálózaton keresztül 20 000 nanoszekundum
RAM 1 MB-ot olvasson be egymás után a memóriából 250 000 nanoszekundum
Korong letöltés az új lemez helyéről (keresés) 8 000 000 nanoszekundum (8 ms)
Korong 1 MB-ot olvas fel egymás után a lemezről 20 000 000 nanoszekundum (20 ms)
Hálózat küldje el az USA csomagját Európába és vissza 150 000 000 nanoszekundum (150 ms)

Peter Norvig ezeket a számokat először néhány évvel ezelőtt tette közzé a Tanítsd meg magad programozást tíz év alatt c. Mivel a számítógépek és alkatrészeik évről évre változnak, nem a fenti pontos számok a lényeg. Amit ezek a számok segítenek szemléltetni, az a műveletek közötti nagyságrendbeli különbség.

Hasonlítsa össze a különbséget a fő memóriából történő letöltés és az egyszerű csomag interneten keresztüli küldése között. Bár mindkét művelet emberi szempontból kevesebb, mint egy szempillantás alatt (szó szerint) történik, láthatja, hogy egy egyszerű csomag interneten keresztüli küldése több mint egymilliószor lassabb, mint a RAM-ból való lehívás. Különbség, hogy az egyszálas programban gyorsan felhalmozódhat, hogy problémás szűk keresztmetszeteket képezzen.

A Hydra-ban a válaszadatok elemzése és az eredmények jelentésbe foglalása viszonylag gyors, mivel mindez a CPU-n történik. A program végrehajtásának leglassabb része, több mint hat nagyságrenddel, a hálózati késés. A Hydra-nak nem csak csomagokat, hanem egész weboldalakat kell beolvasnia!

A Hydra teljesítményének javításának egyik módja az, hogy megtalálja a módot, amellyel az oldalletöltési feladatok végrehajthatók a fő szál blokkolása nélkül.

A Pythonnak pár lehetősége van a feladatok párhuzamos elvégzésére: több folyamat vagy több szál. Ezek a módszerek lehetővé teszik a GIL megkerülését és a végrehajtás felgyorsítását pár különböző módon.

Több folyamat

Párhuzamos feladatok több folyamat segítségével történő végrehajtásához használhatja a Python-okat ProcessPoolExecutor. A modul konkrét alosztálya Executora concurrent.futuresmodullal ProcessPoolExecutorlétrehozott folyamatok készletét használja multiprocessinga GIL elkerülésére.

Ez az opció olyan munkavállalói alfolyamatokat használ, amelyek maximálisan alapértelmezés szerint a gépen lévő processzorok számát adják meg. A multiprocessingmodul lehetővé teszi a folyamatok maximális párhuzamosítását a folyamatok között, ami valóban felgyorsíthatja a számításhoz kötött (vagy a processzorhoz kötött) feladatokat.

Mivel a Hydra fő szűk keresztmetszete az I / O, és nem a CPU által végrehajtandó feldolgozás, jobban szolgál, ha több szálat használok.

Több szál

Megfelelően elnevezve a Python's szálkészletet ThreadPoolExecutorhasznál aszinkron feladatok végrehajtásához. A (z) alosztálya szintén Executormeghatározott számú maximális dolgozói szálat használ (alapértelmezés szerint legalább ötöt, a képlet szerint min(32, os.cpu_count() + 4)), és újból felhasználja a tétlen szálakat, mielőtt újakat indítana, ezáltal elég hatékony.

Itt van egy részlet a Hydra-ból, amely megjegyzi, hogy Hydra hogyan használja ThreadPoolExecutora párhuzamos, többszálú boldogságot:

# Create the Checker class class Checker: # Queue of links to be checked TO_PROCESS = Queue() # Maximum workers to run THREADS = 100 # Maximum seconds to wait for HTTP response TIMEOUT = 60 def __init__(self, url): ... # Create the thread pool self.pool = futures.ThreadPoolExecutor(max_workers=self.THREADS) def run(self): # Run until the TO_PROCESS queue is empty while True: try: target_url = self.TO_PROCESS.get(block=True, timeout=2) # If we haven't already checked this link if target_url["url"] not in self.visited: # Mark it as visited self.visited.add(target_url["url"]) # Submit the link to the pool job = self.pool.submit(self.load_url, target_url, self.TIMEOUT) job.add_done_callback(self.handle_future) except Empty: return except Exception as e: print(e) 

A teljes kódot megtekintheti a Hydra GitHub adattárában.

Egyszálas többszálas

Ha szeretné látni a teljes hatást, összehasonlítottam a webhelyem ellenőrzésének futási idejét az egyszálas prototípusú program és a többfejű - mármint többszálas - Hydra között.

time python3 slow-link-check.py //victoria.dev real 17m34.084s user 11m40.761s sys 0m5.436s time python3 hydra.py //victoria.dev real 0m15.729s user 0m11.071s sys 0m2.526s 

Az egyszálú program, amely blokkolja az I / O-t, tizenhét perc alatt futott. Amikor először futtattam a többszálas verziót, 1m13.358 másodperc alatt elkészült - némi profilozás és hangolás után valamivel kevesebb, mint tizenhat másodpercig tartott.

A pontos idők megint nem jelentenek annyit; ezek olyan tényezőktől függően változnak, mint a feltérképezett webhely mérete, a hálózati sebesség, valamint a program egyensúlya a szálkezelés általános költségei és a párhuzamosság előnyei között.

A legfontosabb dolog, és az eredmény, amelyet minden nap el fogok venni, egy olyan program, amely néhány nagyságrenddel gyorsabban fut.