Daugiasluoksnis VS daugiaprocesinis procesas Python'e

Atskleisti tikrąjį daugiaplanio veido veidą

Šiame straipsnyje pabandysiu aptarti keletą klaidingų nuomonių apie „Multithreads“ ir paaiškinti, kodėl jos klaidingos.
Visi eksperimentai atliekami mašina su 4 šerdimis (EC2 c5.xlarge).

Pythonai mėgaujasi jaukiu gijų baseinu.

Aš ilgą laiką nagrinėjau lygiagretumą python'e ir nuolat skaitydavau straipsnius bei perviršio siūlus, kad galėčiau geriau suprasti šią temą. Paprastai kuo daugiau ieškai, tuo daugiau išmoksti. Tačiau daugiasluoksnis / daugiaprocesinis procesas, kuo daugiau ieškojau, tuo labiau susipainiojau. Štai pavyzdys:

Nors pirmoji atsakymo dalis teisinga, paskutinė yra visiškai klaidinga.
Aš nesiveržiu į atsakymą parašiusio žmogaus, atvirkščiai: aš labai gerbiu visus, kurie bando padėti kitiems žmonėms. Aš tik pasinaudojau šiuo pavyzdžiu norėdamas parodyti, kad kai kurie daugelio siūlų paaiškinimai gali būti klaidinantys. Be to, kai kuriuose kituose paaiškinimuose vartojami išplėstiniai terminai ir tai gali padaryti viską sunkiau, nei yra iš tikrųjų.

PS: Aš stengsiuosi, kad viskas būtų paprasta: taigi, nereikia kalbėti apie GIL, atmintį, ėsdinimą, pridėtines dalis. (Nors aš šiek tiek kalbėsiu apie pridėtines išlaidas).

Pradėkime!

Daugiaprocesinis apdorojimas ir daugiasluoksnis iš esmės yra tas pats dalykas.

FALSE!

[Susieti visą eksperimento kodą]

Pradėsiu nuo paprasto eksperimento ir pasiskolinsiu kodą iš šio straipsnio, kurį parašė Brendanas Fortuneris, kuris, be abejo, yra puikus skaitymas.

Tarkime, kad turime šią užduotį, kurią vykdysime daugybę kartų.

def cpu_heavy (x):
    spausdinti („Aš esu“, x)
    skaičius = 0
    i diapazonui (10 ** 8):
        skaičius + = i

Kitas bandysime tiek daugiaprocesinį, tiek daugiaplanį

iš „concurrent.futures“ importuoti „ProcessPoolExecutor“, „ThreadPoolExecutor“
daugiasluoksnis (func, args, darbininkai):
    su „ThreadPoolExecutor“ (darbuotojais) kaip ex:
        res = ex.map (func, args)
    grąžinti sąrašą (res)
def multiprocessing (func, args, darbininkai):
    su „ProcessPoolExecutor“ (darbuotojais), pvz .:
        res = ex.map (func, args)
    grąžinti sąrašą (res)

Atminkite, kad jei įgyvendinsite:
- Daugiaprocesinis apdorojimas su daugiaprocesiniu arba vienu metu vykstančiais procesais.
- Daugiasluoksnis su sriegimu ar daugiaprocesiniu diegimu.
tai neturės įtakos mūsų eksperimentams.

Paleiskime šiek tiek kodo:

„visualize_runtimes“ (daugiasluoksnis („cpu_heavy“, diapazonas (4), 4))
„visualize_runtimes“ (daugiaprocesinis apdorojimas („cpu_heavy“, diapazonas (4), 4))

Nors „Multithreading“ užtruko 20 sekundžių, „Multiprocessing“ užtruko tik 5 sekundes.

Taigi dabar, kai esame įsitikinę, kad jie nėra tie patys, norėtume sužinoti, kodėl. Pereikime prie kito klaidingo supratimo apie daugybinį siūlą.

Daugiagijyje siūlai eina lygiagrečiai.

FALSE!
Faktiškai „ThreadPool“ programoje bet kuriuo metu t vykdoma tik viena gija.

[Susieti visą eksperimento kodą]

Aš nežinau apie tave, bet man tai buvo šokas!
Aš visada galvojau, kad gijos vienu metu vykdo kodą, bet tai yra netiesa Pyhtone.

Padarykime nedidelį eksperimentą. Skirtingai nuo ankstesnio, mes ne tik stebėsime kiekvieno darbo pradžią ir sustabdymą, bet ir kiekvieną laiko momentą, kuriame vykdomas darbas:

def live_tracker (x):
    spausdinti („Aš esu“, x)
    l = []
    i diapazonui (10 ** 6):
        l.append (laikas.time ())
    grąžinti l

Kaip ir anksčiau, vykdysime eksperimentą ir pateiksime naujus grafikus.

„visualize_live_runtimes“ (daugiasluoksnis („live_tracker“, diapazonas (4), 4))
„visualize_live_runtimes“ (daugiaprocesinis apdorojimas („live_tracker“, diapazonas (4), 4))

Tiesą sakant, siūlai nejuda nei lygiagrečiai, nei paeiliui. Jie veikia vienu metu! Kiekvieną kartą vienas darbas bus šiek tiek vykdomas, o kitas imsis.

Lygiagretiškumas ir paralelizmas yra susiję terminai, bet ne tie patys ir dažnai klaidingai suprantami kaip panašūs terminai. Esminis skirtumas tarp lygiavertiškumo ir paralelizmo yra tas, kad lygiagretiškumas yra susijęs su daugelio dalykų tuo pačiu metu (sukuria vienalaikiškumo iliuziją) arba tuo pačiu metu vykstančių įvykių, iš esmės slepiančių vėlavimą, tvarkymu. Atvirkščiai, lygiagretumas reiškia, kad vienu metu daroma daug dalykų, norint padidinti greitį. [Šaltinis: techdifferences.com]

Turint tai mintyje, jei turite sunkią CPU užduotį ir norite ją greičiau padaryti naudodami daugialypį apdorojimą!
Pvz., Jei turite 4 gyslas, tokias, kokias aš dariau atliekant testus, su daugybe sriegių kiekviena šerdis bus maždaug 25% talpos, o atliekant daugiaprocesinį apdorojimą jūs gausite 100% kiekvienos šerdies. Tai reiškia, kad 100% panaudodami 4 gyslas, padidinsite greitį 4. Kaip būtų 25% daugiasriegių? ar pasieksime greitį? Atsakymas kitame skyriuje.

Daugiasluoksnis siuvimas visada yra greitesnis nei serijinis.

FALSE!
Tiesą sakant, sunkioms CPU užduotims, daugybinis siuvimas ne tik nieko gero neatneš. Blogiausia: jūsų kodas taps dar lėtesnis!

[Susieti visą eksperimento kodą]

Jei išsiųsite sunkią CPU užduotį į keletą gijų, tai nebus pagreitinta. Priešingai, tai gali pabloginti bendrą našumą.
Įsivaizduokite tai taip: jei turite 10 užduočių ir kiekviena trunka 10 sekundžių, serijos vykdymas iš viso užtruks 100 sekundžių. Tačiau naudojant daugybinius siūlus, nes bet kuriuo metu t vykdoma tik viena gija, tai bus tarsi nuoseklusis vykdymas Plius, skirtas perjungti tarp gijų.

Taigi eksperimentui aš pradedu 4 sunkius procesorių darbus, naudodamasis 4 gyslomis, turinčiomis 4 branduolių mašiną (EC2 c5.xlarge), ir lygindamas ją su serijiniu vykdymu.

def cpu_heavy (x):
    skaičius = 0
    i diapazonui (10 ** 10):
        skaičius + = i


n_jobs = 4

žymeklis = laikas.time ()
i diapazonui (n_jobs): cpu_heavy (i)
spausdinti („Serial praleido“, time.time () - žymeklis)
žymeklis = laikas.time ()
daugiasluoksnis (cpu_heavy, diapazonas (n_jobs), 4)
spausdinti („Daugialypiai praleisti“, laikas.time () - žymeklis)

Išėjimai:

amine @ c5-xlarge: ~ $ python3 eksperiment.py
Serialas išleistas 1658.8452804088593
Daugiasluoksnė išleista 1668,857419490814

Taigi „Multithreading“ yra 10 sekundžių lėtesnis nei „Serial“ atliekant sudėtingas CPU užduotis, net turint 4 siūlus 4 branduolių mašinoje.

Tiesą sakant, skirtumas yra nereikšmingas, nes 27 minutes trunkantis darbas trunka 10 sekundžių (0,6 proc. Lėčiau), tačiau vis tiek tai rodo, kad daugiagijų siuvimas šiuo atveju nenaudingas.

Ar tada daugiasluoksnis yra geras?

Daugiasluoksnis yra nenaudingas.

FALSE!
Tiesą sakant, atliekant sudėtingas CPU užduotis, daugiasluoksnis iš tikrųjų yra nenaudingas. Tačiau jis puikiai tinka IO.

[Susieti visą eksperimento kodą]

Vykdydamas IO užduotis, pavyzdžiui, užklausdamas duomenų bazę ar įkeldamas tinklalapį, centrinis procesorius tik nedaro nieko, o laukia atsakymo. Pabandykime atlikti 16 užklausų iš eilės, nenaudodami 4 gijų, tada naudokite 8:

URL = [...] # 16 URL
def load_url (x):
    su urllib.request.urlopen (URL [x], laikas = 5) kaip jungtis:
        grįžti conn.read ()


n_jobs = len (URL)

žymeklis = laikas.time ()
i diapazone (n_jobs): load_url (i)
spausdinti („Serial praleido“, time.time () - žymeklis)
žymeklis = laikas.time ()
daugiasluoksnis (load_url, diapazonas (n_jobs), 4)
spausdinti („Skirtas daug kartų praleisti 4“, laikas.time () - žymeklis)
žymeklis = laikas.time ()
daugiasriegis (load_url, diapazonas (n_jobs), 8)
spausdinti („Daugialypiai 8 praleisti“, laikas.time () - žymeklis)

Išėjimai

amine @ c5-xlarge: ~ $ python3 serial_comparaison_io.py
Serialas išleistas 7.8587799072265625
Daugiasluoksnis su 4 išleistais 2.5494980812072754
Daugiasluoksnis su 8 išleistais 1.1110448837280273
Daugiasluoksnis su 16 išleistų 0.6199102401733398

Atkreipkite dėmesį, kad mes pastebėjome greitą pagreitį, palyginti su serijiniais daugybiniais siūlais! Taip pat atminkite, kad kuo daugiau gijų turite, tuo greičiau vykdysite. Žinoma, nėra prasmės turėti daugiau gijų nei URL skaičius, todėl aš sustojau ties 16 gijų, skirtų 16 URL.

Taip pat atminkite, kad geriausiu atveju, vykdant daugybę siūlų, vykdymo laikas yra lygus maksimaliam laiko praleidimui, įkeliant vieną URL: Jei turite 16 URL, kurių įkėlimas trunka 10 sekundžių, o 15 kitų - po 0,1 sekundės, jei naudosite 8 gijų siūlų fondą, jūsų programa truks mažiausiai 10 sekundžių, o nuoseklioji programa - 11,5 sekundės. Taigi šiuo atveju nėra didelis paspartinimas.

Gerai, kad dabar mes žinome, kad net daugialypis sriegis kenkia procesoriui, tačiau jis puikiai veikia IO.

Jei daugialypis sriegis yra blogas procesoriui ir geras IO, ar tai reiškia, kad daugiaprocesinis apdorojimas yra naudingas procesoriui, o blogas - IO?
Atsakymas kitame skyriuje.

Daugiaprocesinis apdorojimas kenkia IO.

FALSE!

Kalbant apie IO, daugiaprocesinis apdorojimas iš esmės yra toks pat geras kaip ir daugiasluoksnis. Tiesiog jis turi daugiau pridėtinių išlaidų, nes iššokantys procesai yra brangesni nei iššokantys siūlai.

Jei jums patinka atlikti eksperimentą, tiesiog pakeiskite daugiasriegį ankstesniame daugiaprocesiniu apdorojimu.

amine @ c5-xlarge: ~ $ python3 serial_comparaison_io.py
Serialas išleistas 5,325972080230713
Daugiaprocesinis 4 išleistas 1.2662420272827148
Multiprocessing 8 išleido 0.8015711307525635
Daugiaprocesinis 16 išleido 0,5572431087493896

[Premija] Kelių procesų apdorojimas visada yra greitesnis nei serijinis.

TIESA, bet tik jūs viską darote teisingai

Pvz., Jei turite 1000 cpu sunkią užduotį ir turite tik 4 branduolius, neišmeskite daugiau nei 4 procesų, kitaip jie varžysis dėl procesoriaus išteklių.
(konkuruoti => varžytis => kartu)

Išvada

  • Python procese tam tikru metu gali būti vykdoma tik viena gija.
  • Multiprocessing yra lygiagretumas. Daugybė siūlų yra lygiagrečiai.
  • Multiprocessing yra skirtas padidinti greitį. Daugiasluoksnis skirtas paslėpti latenciją.
  • Daugiausia apdorojimas yra geriausias skaičiavimams. Daugiasluoksnis yra geriausias IO.
  • Jei turite sunkias CPU užduotis, naudokite daugialypį apdorojimą su n_process = n_cores ir niekada daugiau. Niekada!
  • Jei turite IO sunkių užduočių, naudokite daugiasriegius su n_threads = m * n_core, kurių m skaičius yra didesnis nei 1, kurį galite patikslinti patys. Išbandykite daugybę verčių ir išsirinkite geriausią, kur nėra greičio, nes nėra bendros taisyklės. Pavyzdžiui, numatytoji m reikšmė „ThreadPoolExecutor“ yra 5 [Šaltinis], kuri, mano manymu, atrodo gana atsitiktinė.

Viskas.

Nuorodos