MC, 2025
Ilustracja do artykułu: Python GIL explained: paslaptis, kuri keičia viską

Python GIL explained: paslaptis, kuri keičia viską

Jeigu kada nors bandėte parašyti daugiafunkcinę (multithreaded) programą su Python, tikriausiai susidūrėte su keistu elgesiu – kodas nenaudoja visų procesoriaus branduolių taip, kaip tikėjotės. Priežastis slypi viename gana mįslingame mechanizme, vadinamame GIL. Šis straipsnis "python GIL explained" atskleis, kas yra GIL, kodėl jis egzistuoja, ir pateiks python GIL explained przykłady, kurie padės tai geriau suprasti.

Kas yra GIL?

GIL – tai „Global Interpreter Lock“, t.y. globalus interpretatoriaus užraktas. Tai mechanizmas, kuris leidžia tik vienai gijai (thread) vykdyti Python baitų kodą bet kuriuo metu. Nors jūs galite sukurti kelias gijas, GIL užtikrina, kad vienu metu tik viena iš jų galės aktyviai vykdyti Python kodą.

Kodėl GIL egzistuoja?

GIL buvo įdiegtas tam, kad būtų lengviau valdyti atminties paskirstymą CPython'e – tai yra pagrindinė Python interpretatoriaus implementacija. Python naudoja referencinį skaičiavimą objektų gyvavimo ciklui, ir tam reikia saugaus būdo valdyti bendrą atmintį. GIL supaprastina šį procesą, nes leidžia tik vienai gijai dirbti su atmintimi vienu metu.

GIL privalumai

Nors GIL turi daug kritikų, jis taip pat turi keletą privalumų:

  • Supaprastina Python branduolio kodą
  • Padeda išvengti sudėtingų sinchronizavimo klaidų
  • Tinka viengijai (single-threaded) programoms

GIL trūkumai

Tačiau, kaip ir galima tikėtis, yra ir rimtų minusų:

  • Blogai veikia daugiafunkciniuose (multithreaded) scenarijuose
  • Neleidžia pilnai išnaudoti daugiabranduolių CPU
  • Gali sukelti spartos praradimą, kai gijos konkuruoja dėl GIL

python GIL explained przykłady: paprasta demonstracija

Štai paprastas pavyzdys, kaip GIL riboja tikrąjį lygiagretumą:

import threading
import time

def skaičiuok():
    for _ in range(10**7):
        pass

start = time.time()
g1 = threading.Thread(target=skaičiuok)
g2 = threading.Thread(target=skaičiuok)
g1.start()
g2.start()
g1.join()
g2.join()
print("Laikas su gijom:", time.time() - start)

Galbūt tikėjote, kad tai vyks dvigubai greičiau nei vienas skaičiavimas, tačiau rezultatas beveik identiškas arba net lėtesnis nei vienos gijos atveju. Kodėl? GIL leidžia vienai gijai dirbti, o kitai – laukti.

Alternatyva: multiprocessing

Python siūlo apeiti GIL apribojimus naudojant multiprocessing modulį, kuris naudoja atskirus procesus vietoj gijų. Kadangi kiekvienas procesas turi savo GIL, jie gali dirbti tikrai lygiagrečiai:

from multiprocessing import Process
import time

def skaičiuok():
    for _ in range(10**7):
        pass

start = time.time()
p1 = Process(target=skaičiuok)
p2 = Process(target=skaičiuok)
p1.start()
p2.start()
p1.join()
p2.join()
print("Laikas su multiprocessing:", time.time() - start)

Šis metodas leidžia pilnai išnaudoti kelis branduolius ir dažnai gerokai paspartina skaičiavimus.

python GIL explained przykłady su IO operacijomis

GIL neturi tokios didelės įtakos IO-bound (įvesties/išvesties ribojamiems) užduotims, kaip failų skaitymas ar tinklo užklausos. Taip yra todėl, kad Python atiduoda GIL, kai vyksta „blocking“ IO operacijos. Pvz.:

import threading
import time

def lauk():
    time.sleep(2)
    print("Baigta!")

start = time.time()
g1 = threading.Thread(target=lauk)
g2 = threading.Thread(target=lauk)
g1.start()
g2.start()
g1.join()
g2.join()
print("Vykdymo laikas:", time.time() - start)

Rezultatas bus apie 2 sekundės, o ne 4, nes gijos laukdamos IO nepriklauso nuo GIL.

Kas būtų be GIL?

Python bendruomenė dažnai diskutuoja apie GIL pašalinimą. Buvo projektų kaip Gilectomy (eksperimentas pašalinti GIL) ar alternatyvių implementacijų, kaip Jython arba IronPython, kurios neturi GIL. Tačiau jos nepalaiko visų CPython bibliotekų arba nėra plačiai naudojamos.

Kaip elgtis su GIL?

Jei kuriate Python programą, turėtumėte pasirinkti strategiją, kaip apeiti GIL:

  • Naudoti multiprocessing vietoje threading
  • Perduoti sunkius skaičiavimus C/C++ arba Cython
  • Vengti gijų ten, kur jos nėra būtinos

Naudoti C biblioteka? Gera mintis!

Moduliai kaip NumPy ar pandas dažnai parašyti C kalba ir paleidžia intensyvų skaičiavimą už Python ribų, todėl GIL jų beveik neveikia. Jei įmanoma, deleguokite sunkius darbus tokioms bibliotekoms.

GIL ir asinchroninis programavimas

Dar viena strategija – naudoti asyncio modulį. Asinchroninis programavimas leidžia efektyviai valdyti daugybę užduočių, kurios dažnai laukia atsako, pvz., tinklo užklausos:

import asyncio

async def dirbk():
    await asyncio.sleep(2)
    print("Atlikta!")

async def main():
    await asyncio.gather(dirbk(), dirbk())

asyncio.run(main())

Asinchroninis kodas gali būti efektyvesnis IO-bound situacijose ir nekenčia nuo GIL tiek, kiek CPU-bound kodas.

Apibendrinimas

Šis straipsnis "python GIL explained" parodė, kad GIL – tai ne blogis, o kompromisas tarp paprastumo ir spartos. Jei dirbate su viengijėmis ar IO-bound užduotimis, GIL jums netrukdo. Tačiau, jei siekiate tikros lygiagretos skaičiavimo galios, turite ieškoti kitų būdų, kaip aplenkti jo apribojimus. Su python GIL explained przykłady ir praktiniais patarimais jūs dabar turite gerą startą dirbant su Python daugiagijais scenarijais.

Komentarze (0) - Nikt jeszcze nie komentował - bądź pierwszy!

Imię:
Treść: