Mitme niidiga Python: kas libiseda läbi I / O kitsaskoha?

Kuidas Pythonis paralleelsuse ärakasutamine võib muuta teie tarkvara suurusjärgu kiiremaks.

Ma töötasin hiljuti välja projekti, mida kutsusin Hydraks: Pythonis kirjutatud mitmekeermelise lingi kontrollija. Erinevalt paljudest Pythoni saidirobotitest, mille uurimise käigus leidsin, kasutab Hydra ainult tavalisi teeke, millel pole väliseid sõltuvusi nagu BeautifulSoup. See on mõeldud käivitamiseks CI / CD protsessi osana, nii et osa selle edust sõltus kiirusest.

Mitu lõime Pythonis on natuke närviline teema (pole kahju), kuna Pythoni tõlk ei lase tegelikult mitut lõime korraga käivitada.

Pythoni globaalne tõlgenduslukk ehk GIL takistab mitmel niidil Pythoni baidekoodide korraga täitmist. Iga käivitatav lõim peab kõigepealt ootama, kuni GIL vabastatakse parajasti käivitatava lõime poolt. GIL on üsna väikese eelarvega konverentsipaneeli mikrofon, välja arvatud juhul, kui keegi ei saa karjuda.

Selle eeliseks on võistlustingimuste vältimine. Sellel puuduvad aga jõudluse eelised, mida pakuks mitme ülesande paralleelne käivitamine. (Kui soovite värskendust samaaegsusele, paralleelsusele ja mitmikeermelisusele, vaadake artiklit Samaaegsus, paralleelsus ja paljud jõuluvana niidid.)

Kui ma eelistan Go-d selle mugavate esmaklassiliste primitiivide eest, mis toetavad samaaegsust (vt Goroutines), olid selle projekti saajad Pythoniga mugavamad. Võtsin seda kui võimalust katsetada ja uurida!

Mitme ülesande samaaegne täitmine Pythonis pole võimatu; see nõuab lihtsalt väikest lisatööd. Hydra jaoks on peamine eelis sisend / väljund (I / O) kitsaskoha ületamine.

Veebilehtede kontrollimiseks peab Hydra minema Internetti ja neid tooma. Võrreldes ülesannetega, mida täidab ainult protsessor, on võrgu kaudu väljumine suhteliselt aeglasem. Kui aeglane?

Siin on tavalises arvutis tehtavate toimingute ligikaudne ajastus:

Ülesanne Aeg
Protsessor täita tüüpilisi juhiseid 1/1 000 000 000 sekundit = 1 nanosek
Protsessor tooma L1 vahemälust 0,5 nanosek
Protsessor haru valesti ennustamine 5 nanosek
Protsessor tooma L2 vahemälust 7 nanosek
RAM Mutexi lukustamine / avamine 25 nanosek
RAM tooma põhimälust 100 nanosek
Võrk saata 2K baiti 1Gbps võrgu kaudu 20 000 nanosek
RAM lugege mälust järjest 1 MB 250 000 nanosek
Ketas tooma uuelt ketta asukohalt (otsi) 8 000 000 nanosek (8 ms)
Ketas 1MB lugemine järjest kettalt 20 000 000 nanosek (20 ms)
Võrk saatke USA pakett Euroopasse ja tagasi 150 000 000 nanosek (150 ms)

Peter Norvig avaldas need numbrid esimest korda mõned aastad tagasi ajakirjas Teach Yourself Programming Kümne aasta jooksul. Kuna arvutid ja nende komponendid muutuvad aasta-aastalt, pole ülaltoodud täpsed arvud selles olulised. Need arvud aitavad illustreerida operatsioonide erinevust suurusjärkudes.

Võrrelge erinevust põhimälust toomise ja lihtsa paketi Interneti kaudu saatmise vahel. Ehkki mõlemad toimingud toimuvad inimese vaatenurgast vähem kui silmapilk (sõna otseses mõttes), näete, et lihtsa paketi saatmine Interneti kaudu on üle miljoni korra aeglasem kui RAM-ist toomine. See on erinevus, mis ühe lõimega programmis võib kiiresti koguneda, et moodustada tülikad kitsaskohad.

Hydras on vastuseandmete parsimise ja tulemuste aruandeks koondamise ülesanne suhteliselt kiire, kuna see kõik toimub protsessoril. Aeglasem osa programmi käivitamisest, üle kuue suurusjärgu, on võrgu latentsus. Mitte ainult Hydra ei pea pakkima pakette, vaid ka terveid veebilehti!

Üks viis Hydra jõudluse parandamiseks on leida viis, kuidas lehe toomise ülesanded saaksid täita ilma peaniiti blokeerimata.

Pythonis on ülesannete paralleelseks tegemiseks paar võimalust: mitu protsessi või mitu lõime. Need meetodid võimaldavad teil GIL-ist mööda hiilida ja kiirendada täitmist paaril erineval viisil.

Mitu protsessi

Paralleelsete ülesannete täitmiseks mitme protsessi abil saate kasutada Pythoni funktsioone ProcessPoolExecutor. Moodulist Executorpärit konkreetne alaklass kasutab GIL-i vältimiseks mooduliga sündinud protsesside kogumit .concurrent.futuresProcessPoolExecutormultiprocessing

See suvand kasutab töötajate alamprotsesse, mis vaikimisi määravad masina protsessorite arvu. multiprocessingMoodul võimaldab maksimaalselt paralleelseks funktsiooni täitmise üle protsesside kohta, mis võib tõesti kiirendada arvutama seotud (või CPU-seotud) ülesandeid.

Kuna Hydra peamine kitsaskoht on sisend / väljund, mitte protsessori tehtav töötlus, teeniks mind paremini mitme lõime kasutamine.

Mitu lõime

Sobiva nimega ThreadPoolExecutorkasutab Python's asünkroonsete ülesannete täitmiseks lõimude kogumit. Ka selle alamklass Executorkasutab määratletud arvu maksimaalselt töölisi (vastavalt valemile vaikimisi vähemalt viis min(32, os.cpu_count() + 4)) ja taaskasutab tühikäike enne uute alustamist, muutes selle üsna tõhusaks.

Siin on fragment Hydrast koos kommentaaridega, mis näitavad, kuidas Hydra kasutab ThreadPoolExecutorparalleelselt mitme niidiga õndsust:

# 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) 

Täielikku koodi saate vaadata Hydra GitHubi hoidlas.

Ühe niidiga mitmekeermeline

Kui soovite näha täielikku efekti, võrdlesin oma veebisaidi kontrollimise käitamisaegu üheahelalise prototüübi programmi ja mitmepealise - ma mõtlen mitmikeermelise - Hydra vahel.

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 

Üheahelaline programm, mis blokeerib sisend / väljund, käis umbes seitseteist minutit. Kui ma esimest korda mitmikeermelist versiooni käitusin, sai see valmis 1m13.358s - pärast mõningast profileerimist ja häälestamist võttis see aega veidi alla kuusteist sekundit.

Jällegi ei tähenda täpsed ajad nii palju; need varieeruvad sõltuvalt sellistest teguritest nagu roomatava saidi suurus, teie võrgu kiirus ja teie programmi tasakaal niidi haldamise üldkulude ja paralleelsuse eeliste vahel.

Tähtsam asi ja tulemus, mille ma iga päev võtan, on programm, mis töötab mõne suurusjärgu võrra kiiremini.