Sii Polska

SII UKRAINE

SII SWEDEN

  • Szkolenia
  • Kariera
Dołącz do nas Kontakt
Wstecz

Sii Polska

SII UKRAINE

SII SWEDEN

Wstecz
Python bez GIL-a

„…O co chodzi, o co chodzi…”

Piotrek Szumowski w programie Enzymy i Pioruny

No właśnie, o co chodzi? GIL (Global Interpreter Lock) to mechanizm bezpieczeństwa wprowadzony w CPythonie w celu uproszczenia zarządzania pamięcią w intrepreterze. Zapewnia, że tylko jeden wątek może wykonywać kod bajtowy Pythona w danym momencie.

Skutki? Nawet jeśli mamy 32-rdzeniowy procesor, klasyczne wątki w Pythonie nie potrafią w pełni wykorzystać go do obliczeń. W efekcie, przez lata rekomendowanym sposobem na równoległe przetwarzanie zadań obciążających procesor były procesy (ang. multiprocessing).

Jednak każde z tych podejść ma swoje zalety i ograniczenia:

  • Wątki – lekkie, szybkie w tworzeniu, współdzielą pamięć, ale skutecznie ograniczone przez GIL w kontekście CPU-bound.
  • Procesy – działają niezależnie, omijają GIL, ale kosztują więcej zasobów (czas uruchomienia, kopiowanie danych).

W wersji Python 3.13 pojawia się eksperymentalna możliwość wyłączenia GIL-a – i właśnie to przetestuję w tym artykule.

Instalacja/Konfiguracja

Nie możemy jeszcze oficjalnie zainstalować wersji Pythona z opcją wyłączania GIL-a. Sam autor – Sam Gross – w swoim repozytorium odsyła w celu instalacji na stronę Python Free-Threading Guide.

Aby zainstalować Pythona w wersji bez GIL-a, mamy do wyboru:

Wybrałem opcję instalacji Pythona bez GIL-a poprzez instalator Pythona.

Dodatkowa opcja instalacji Pythona w wersji bez GIL-a
Ryc. 1 Dodatkowa opcja instalacji Pythona w wersji bez GIL-a

W miejscu instalacji znajdować będzie się wersja docelowa Pythona oraz wersja z możliwością wyłączenia GIL-a.

Python w wersji oficjalnej oraz eksperymentalnej w folderze instalacyjnym
Ryc. 2 Python w wersji oficjalnej oraz eksperymentalnej w folderze instalacyjnym

Co sprawdzimy?

W tym artykule nie poprzestanę na teorii. Przeprowadzę praktyczne porównanie trzech trybów współbieżnej pracy:

  • Wątki w standardowym CPythonie z włączonym GIL-em.
  • Procesy w CPythonie.
  • Wątki w eksperymentalnym CPythonie z wyłączonym GIL-em.

Dla każdego z przypadków przeanalizuję czas wykonania zadań obliczeniowych. Celem jest sprawdzenie, czy wyłączenie GIL-a rzeczywiście przynosi realny zysk w kontekście wielowątkowości.

Środowisko testowe

Do przeprowadzenia badań wykorzystałem Procesor AMD Ryzen 5 7500F mający 6 rdzeni i 12 wątków, 32GB RAM na Windowsie 11. Kod do badań został uruchomiony w PyCharmie z wykorzystaniem Pythona w wersji 3.13.5 i 3.13.5t.

Test 1.

W pierwszym teście użyty został prosty wzór wykorzystujący obliczenia arytmetyczne:

image3 - Python bez GIL-a

Rysunki poniżej przedstawiają porównanie czasu wykonania 1 000 000 operacji arytmetycznych przy użyciu różnych metod współbieżności: procesów oraz wątków z włączonym i wyłączonym GIL-em.

Czas pracy 4 procesów
Ryc. 3 Czas pracy 4 procesów
Porównanie czasów pracy 4 wątków z włączonym i wyłączonym GIL-em
Ryc. 4 Porównanie czasów pracy 4 wątków z włączonym i wyłączonym GIL-em
Czas pracy 12 procesów
Ryc. 5 Czas pracy 12 procesów
Porównanie czasów pracy 12 wątków z włączonym i wyłączonym GIL-em
Ryc. 6 Porównanie czasów pracy 12 wątków z włączonym i wyłączonym GIL-em

Na Rysunku 3. widoczna jest wyraźna równoległość pracy procesów – zadania są rozłożone pomiędzy procesy niezależnie, co skutkuje najkrótszym czasem wykonania dla 4 pracowników (około 4,5 s).

Rysunek 4. pokazuje wyraźny wpływ GIL-a na wydajność wątków. W przypadku wątków z aktywnym GIL-em, czas wykonania znacząco wzrasta (około 14,5 s), a zadania są wykonywane niemalże sekwencyjnie. Po jego wyłączeniu czas spada do około 5 s, co wskazuje, że praca wątków jest bardziej równoległa.

Na rysunkach 5. i 6. można również zauważyć, że tworzenie nowych wątków/procesów zajmuje czas, ale nie w przypadku, gdy GIL jest wyłączony. Ich praca zaczyna się praktycznie w tym samym momencie. Ponadto, praca wątków bez GIL-a, kończy się o około pół sekundy szybciej niż procesów.

Test 2.

Drugi test dotyczy obliczania zbioru Mandlebrota. Stosuje on obliczenia z wykorzystaniem liczb urojonych, a rezultatem jest poniższa grafika.

Mandelbrot
Ryc. 7 Mandelbrot

Do przeprowadzonego badania wykorzystano zbiór o wielkości 1200×1200 oraz ograniczeniu iteracji 150.

Czas pracy 4 procesów
Ryc. 8 Czas pracy 4 procesów
Porównanie czasów pracy 4 wątków z włączonym i wyłączonym GIL-em
Ryc. 9 Porównanie czasów pracy 4 wątków z włączonym i wyłączonym GIL-em
Czas pracy 12 procesów
Ryc. 10 Czas pracy 12 procesów
Porównanie czasów pracy 12 wątków z włączonym i wyłączonym GIL-em
Ryc. 11 Porównanie czasów pracy 12 wątków z włączonym i wyłączonym GIL-em

Tym razem, ze względu na to, że praca została podzielona wierszami, pracownicy na górze oraz dole grafiki nie mieli zbyt dużo pracy, co możemy zauważyć na powyższych rysunkach, gdzie czas dla pracowników 0, 1, 2, 9, 10 i 11 jest najkrótszy. Zachowanie wątków i procesów było zbliżone do obserwowanego w pierwszym teście.

Porównanie czasów

Przeglądając powyższe wykresy, zauważyłem, że choć czasy pracy procesów i wątków bez GIL-a kończą się w podobnym momencie, to procesy potrzebują około pół sekundy na utworzenie. W związku z tym postanowiłem przyjrzeć się jeszcze danym, takim jak wartości minimalne, maksymalne i średnie faktycznego czasu pracy.

image17 1 - Python bez GIL-a

Jak zauważyłem, czasy pracy są często lepsze dla procesów, jeśli nie uwzględniamy czasu potrzebnego na ich uruchomienie.

Analiza zużycia CPU i zachowania schedulera w Windowsie

Ostatnią rzeczą, na którą spojrzałem, jest praca procesorów rejestrowana przez system Windows.

Porównanie wykorzystania zasobów procesora przez 1 wątek
Ryc. 12 Porównanie wykorzystania zasobów procesora przez 1 wątek

Widać wyraźnie, że do pracy wykorzystywane są 2 CPU – 10 oraz 11. Dlaczego pracują dwie jednostki CPU, skoro zadanie miał wykonywać tylko jeden wątek? Mój procesor ma 6 rdzeni, a każdy z nich obsługuje 2 wątki (Hyper-Threading lub Simultaneous multithreading). Windows Scheduler w tym przypadku stara się równomiernie rozłożyć obciążenie w ramach jednego rdzenia.

Co jednak, gdy do pracy zaciągniemy 2 wątki?

Zużycie zasobów przez 2 wątki przy wyłączonym GIL-u
Ryc. 13 Zużycie zasobów przez 2 wątki przy wyłączonym GIL-u

Tym razem oba wątki w ramach jednego rdzenia pracują na maksymalnych obrotach. Czy w takim razie możliwe byłoby rozłożenie pracy pomiędzy 2 rdzenie, czyli 4 wątki? Tak, niemniej jest to zależne od algorytmu schedulera w Windowsie oraz tego, jak Windows stara się minimalizować migrację wątków. Wspomniane zachowanie udało mi się zaobserwować przy pracy 8 wątków, gdzie minimalnie mogłyby być zajęte tylko 4 rdzenie.

Zużycie zasobów przez 8 wątków przy wyłączonym GIL-u
Ryc. 14 Zużycie zasobów przez 8 wątków przy wyłączonym GIL-u

Co ciekawe tym razem do pracy zostały zaciągnięte wszystkie rdzenie. Wątki działające na parach logicznych rdzeni (np. CPU0 i CPU1) współdzielą ten sam fizyczny rdzeń. Dlatego część z nich wygląda na mniej aktywną – system rozdziela pracę nierównomiernie, ale faktycznie obciążony jest cały rdzeń.

Praca wszystkich 12 wątków wygląda z kolei następująco:

Porównanie wykorzystania zasobów procesora dla 12 wątków
Ryc. 15 Porównanie wykorzystania zasobów procesora dla 12 wątków

Zgodnie z oczekiwaniami wszystkie procesory są obłożone pracą.

Udało mi się również raz zaobserwować migrację wątków – takie zjawisko może powodować chwilowe spadki wydajności.

Migracja zasobów dla 1 wątku z włączonym GIL-em
Ryc. 16 Migracja zasobów dla 1 wątku z włączonym GIL-em

Podsumowując, Windows skutecznie rozdziela wątki w ramach rdzeni, a przy większej liczbie wątków angażuje wszystkie dostępne jednostki. Widać też, że GIL znacząco ogranicza wykorzystanie dostępnych zasobów.

Niespodziewane problemy

Podczas przygotowywania środowiska do testów, nie obyło się bez problemów.

Biblioteki

Wersja Pythona bez wątków jest niekompatybilna z niektórymi bibliotekami.

kod

Z początku myślałem, że rozwiązaniem tego jest instalacja starszej wersji numpy. W moim przypadku Python==3.13.5 bez wątków wydawał się być niekompatybilny z numpy==2.3.1, przez co zacząłem eksperymentować z starszymi wersjami numpy, jak polecali doświadczeni specjaliści na StackOverflow.

W pewnym momencie wersja numpy==2.2.0 zaczęła działać i myślałem, że downgrade rozwiązał problem. Jednak próba otworzenia środowiska na innym komputerze skończyła się masą frustracji, gdyż żadna wersja nie pomagała mi rozwiązać tego problemu.

Ostatecznie stwierdziłem, że głównym problemem nie była sama wersja numpy, ale próba użycia procesów w Pythonie zbudowanym bez pełnego wsparcia dla nich.

kod

Pomiary czasów

Do pomiarów czasu nie wykorzystałem dokładniejszych narzędzi jak perf_counter() czy cpu_counter() ze względu na to, że korzystają one z własnych punktów początkowych. Bez współdzielonej pamięci może to prowadzić do ujemnych czasów lub błędnych wyników.

W swoim rozwiązaniu postanowiłem wykorzystać time() z racji na to, że bazuje ona na czasie globalnym.

threading i multiprocessing VS concurrent.futures

Dlaczego użyłem ProcessPoolExecutor i ThreadPoolExecutor? Odpowiedź jest bardzo prosta – łatwiejsze zarządzanie maksymalną liczbą wątków lub procesów. Na potrzeby tego badanie nie było konieczności własnej synchronizacji pracy dla poszczególnych pracowników.

Czy usunięcie GIL-a jest bezpieczne?

Nie. Na chwilę obecną jego usunięcie wiąże się z dużymi zmianami w interpreterze CPythona i niesie ryzyko wprowadzenia trudnych do wykrycia błędów, o których Sam wspomina w artykule.

Nawet jeśli uda się zwiększyć wydajność w kodzie wielowątkowym, może to kosztować wydajność w kodzie jednowątkowym, który to dominuje w większości zastosowań Pythona.

Wraz z usunięciem GIL-a pojawią się klasyczne problemy współbieżności. Użytkownicy będą musieli rozważyć ryzyka takie jak wyścigi po zasoby, deadlocki itp.

Autor wspomnianego wyżej artykułu zauważa również, że rozszerzenia napisane w C będą wymagały ponownej kompilacji i/lub modyfikacji, aby były bezpieczne w środowisku bez GIL-a. Część z nich wymagałaby ochrony danych globalnych.

oferty pracy

Podsumowanie

Testy pokazują, że wątki działające bez GIL-a mają duży potencjał – szczególnie w krótkich, lekkich zadaniach (CPU-bound). Tworzą się szybciej niż procesy, a ich czasy wykonania są bardzo zbliżone. Procesy wciąż wygrywają przy dłuższych obciążeniach, ale nowa wersja Pythona to krok w stronę prawdziwej współbieżności.

Dla dogłębniejszego zroumienia różnic odsyłam Was do artykułów:

4.2/5
Ocena
4.2/5
Avatar

O autorze

Adrian Stramski

Software Engineer z 9-letnim doświadczeniem, specjalizujący się w tworzeniu i utrzymaniu rozwiązań backendowych. Magister inżynier WI ZUT oraz mentor, pasjonuje się nowoczesnymi technikami programowania, szczególnie w Pythonie. W wolnych chwilach boulderowiec oraz zagorzały fan planszówek i gier komputerowych

Wszystkie artykuły autora

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Dołącz do nas

Sprawdź oferty pracy

Pokaż wyniki
Dołącz do nas Kontakt

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?