Wielowątkowość w Python z przykładem: Naucz się GIL w Python
⚡ Inteligentne podsumowanie
Wielowątkowość w Python Uruchamia kilka wątków w ramach jednego procesu, dzięki czemu współdzielą one pamięć i działają współbieżnie. Moduł wątkowy tworzy i zarządza tymi wątkami, podczas gdy blokada interpretera globalnego ogranicza rzeczywisty paralelizm, co czyni tę technikę najlepszą w przypadku zadań związanych z wejściem/wyjściem.

Python Język programowania pozwala na korzystanie z przetwarzania wielowątkowego. W tym samouczku dowiesz się, jak pisać aplikacje wielowątkowe w Python.
Co to jest wątek?
Wątek to jednostka wykonania w programowaniu współbieżnym. Wielowątkowość to technika, która pozwala procesorowi wykonywać wiele zadań jednego procesu jednocześnie. Wątki te mogą być wykonywane indywidualnie, współdzieląc jednocześnie zasoby procesowe.
Czym jest proces?
Proces to w zasadzie program w trakcie wykonywania. Gdy uruchamiasz aplikację na komputerze (np. przeglądarkę lub edytor tekstu), system operacyjny tworzy proces.
Na czym polega wielowątkowość Python?
Wielowątkowość w Python Programowanie to znana technika, w której wiele wątków w procesie współdzieli przestrzeń danych z wątkiem głównym, co ułatwia i usprawnia współdzielenie informacji i komunikację w wątkach. Wątki są lżejsze niż procesy. Wiele wątków może być wykonywanych indywidualnie, współdzieląc jednocześnie zasoby procesowe. Celem wielowątkowości jest jednoczesne uruchamianie wielu zadań i funkcji.
Co to jest przetwarzanie wieloprocesowe?
Wieloprocesowe pozwala na jednoczesne uruchomienie wielu niezwiązanych ze sobą procesów. Procesy te nie współdzielą swoich zasobów i komunikują się poprzez IPC.
Python Wielowątkowość a wieloprocesowość
Aby zrozumieć procesy i wątki, rozważmy następujący scenariusz: Plik .exe na Twoim komputerze to program. Po jego otwarciu system operacyjny ładuje go do pamięci, a procesor go uruchamia. Uruchomiona instancja programu nazywa się procesem.
Każdy proces składa się z dwóch podstawowych elementów:
- Code
- Dane
Teraz proces może zawierać jedną lub więcej podczęści zwanych wątki Zależy to od architektury systemu operacyjnego. Wątek można traktować jako sekcję procesu, którą system operacyjny może wykonywać niezależnie.
Innymi słowy, jest to strumień instrukcji, który system operacyjny może uruchamiać niezależnie. Wątki w ramach jednego procesu współdzielą dane tego procesu i są zaprojektowane tak, aby współdziałać, ułatwiając paralelizm.
Dlaczego warto używać wielowątkowości?
Wielowątkowość umożliwia podzielenie aplikacji na wiele podzadań i jednoczesne uruchamianie tych zadań. Jeśli prawidłowo użyjesz wielowątkowości, możesz poprawić szybkość, wydajność i renderowanie aplikacji.
Python Wielowątkowość
Python Obsługuje konstrukcje zarówno dla przetwarzania wielowątkowego, jak i wieloprzetwarzania. W tym samouczku skupisz się przede wszystkim na implementacji wielowątkowy aplikacje z PythonIstnieją dwa główne moduły, których można używać do obsługi wątków w Python:
- wątek moduł i
- gwintowanie moduł
Jednak w PythonIstnieje również coś takiego jak globalna blokada interpretera (GIL). Nie pozwala ona na znaczny wzrost wydajności, a nawet może zmniejszyć wydajność niektórych aplikacji wielowątkowych. Wszystkiego dowiesz się w kolejnych rozdziałach tego poradnika.
Moduły Thread i Threading
Dwa moduły, o których dowiesz się w tym samouczku, to moduł wątku i moduł gwintowania.
Jednakże moduł wątku jest od dawna przestarzały. Zaczynając od Python 3, został on oznaczony jako przestarzały i jest dostępny jedynie jako _nitka dla kompatybilności wstecznej.
Powinieneś użyć wyższego poziomu gwintowanie Moduł dla aplikacji, które zamierzasz wdrożyć. Moduł wątków został tutaj omówiony wyłącznie w celach edukacyjnych.
Moduł wątku
Składnia tworzenia nowego wątku za pomocą tego modułu jest następująca:
thread.start_new_thread(function_name, arguments)
W porządku, teraz omówiłeś podstawową teorię dotyczącą rozpoczęcia kodowania. Więc otwórz swój IDLE lub notatnik i wpisz następujące informacje:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Zapisz plik i naciśnij klawisz F5, aby uruchomić program. Jeśli wszystko zostało wykonane poprawnie, powinieneś zobaczyć następujący wynik:
Więcej na temat sytuacji wyścigu i sposobów radzenia sobie z nimi dowiesz się w kolejnych sekcjach.
WYJAŚNIENIE KODU
- Te polecenia importują moduły czasu i wątku, które służą do obsługi wykonywania i opóźniania Python wątki
- Tutaj zdefiniowałeś funkcję o nazwie test_wątku, który zostanie wywołany przez start_nowy_wątek Metoda. Funkcja wykonuje pętlę while przez cztery iteracje i wyświetla nazwę wątku, który ją wywołał. Po zakończeniu iteracji wyświetla komunikat informujący o zakończeniu wykonywania wątku.
- To jest główna część Twojego programu. Tutaj wystarczy zadzwonić do start_nowy_wątek metoda z test_wątku funkcję jako argument. Spowoduje to utworzenie nowego wątku dla funkcji przekazanej jako argument i rozpoczęcie jej wykonywania. Pamiętaj, że możesz zastąpić ten wątek (thread_test) dowolną inną funkcją, którą chcesz uruchomić jako wątek.
Moduł gwintowania
Ten moduł to implementacja wątków na wysokim poziomie w Python i de facto standard zarządzania aplikacjami wielowątkowymi. Oferuje szeroki zakres funkcji w porównaniu z modułem wątkowym.
Struktura modułu Threading
Oto lista niektórych przydatnych funkcji zdefiniowanych w tym module:
| Nazwa funkcji | OPIS |
|---|---|
| liczba aktywnych() | Zwraca liczbę Wątek przedmioty, które nadal żyją. |
| bieżący wątek() | Zwraca bieżący obiekt klasy Thread. |
| wyliczać() | Wyświetla listę wszystkich aktywnych obiektów Thread. |
| isDaemon() | Zwraca wartość true, jeśli wątek jest demonem. |
| żyje() | Zwraca wartość true, jeśli wątek jest nadal aktywny. |
| Metody klas wątków | |
| początek() | Rozpoczyna aktywność wątku. Należy go wywołać tylko raz dla każdego wątku, ponieważ wielokrotne wywołanie spowoduje błąd wykonania. |
| biegać() | Metoda ta oznacza aktywność wątku i może zostać przesłonięta przez klasę rozszerzającą klasę Thread. |
| Przystąp() | Blokuje wykonanie innego kodu do czasu zakończenia wątku, w którym wywołano metodę Join(). |
Historia: Klasa Thread
Zanim zaczniesz kodować programy wielowątkowe z użyciem modułu wątków, kluczowe jest zrozumienie klasy Thread. Klasa wątku to podstawowa klasa, która definiuje szablon i operacje wątku w Python.
Najczęstszy sposób tworzenia wielowątkowego Python Celem aplikacji jest zadeklarowanie klasy rozszerzającej klasę Thread i nadpisanie jej metody run().
Podsumowując, klasa Thread oznacza sekwencję kodu działającą w osobnym pliku wątek kontroli.
Pisząc aplikację wielowątkową, należy wykonać następujące czynności:
- zdefiniuj klasę rozszerzającą klasę Thread
- Zastąp __init__ konstruktor
- Zastąp biegać() metoda
Po utworzeniu obiektu wątku, początek() metodę można wykorzystać do rozpoczęcia wykonywania tej czynności, a Przystąp() metody można użyć do zablokowania całego innego kodu do czasu zakończenia bieżącego działania.
Teraz spróbujmy użyć modułu wątków do zaimplementowania poprzedniego przykładu. Ponownie uruchom IDLE i wpisz:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
To będzie wynik po wykonaniu powyższego kodu:
WYJAŚNIENIE KODU
- Ta część jest taka sama jak w poprzednim przykładzie. Tutaj importujesz moduły czasu i wątku, które służą do obsługi wykonywania i opóźnień. Python wątki
- W tym fragmencie tworzysz klasę o nazwie tester wątków, która dziedziczy lub rozszerza Wątek Klasa modułu wątków. Jest to jeden z najczęstszych sposobów tworzenia wątków w Python. Należy jednak nadpisać tylko konstruktor i biegać() metoda w Twojej aplikacji. Jak widać w powyższym przykładzie kodu, plik __init__ metoda (konstruktor) została nadpisana. Podobnie nadpisałeś także plik biegać() metoda. Zawiera kod, który chcesz wykonać wewnątrz wątku. W tym przykładzie wywołałeś funkcję thread_test().
- To jest metoda thread_test(), która przyjmuje wartość i jako argument, zmniejsza go o 1 w każdej iteracji i powtarza resztę kodu, aż i stanie się 0. W każdej iteracji drukuje nazwę aktualnie wykonywanego wątku i czeka kilka sekund (co również jest traktowane jako argument).
- thread1 = threadtester(1, „Pierwszy wątek”, 1) Tutaj tworzymy wątek i przekazujemy trzy parametry, które zadeklarowaliśmy w __init__. Pierwszy parametr to identyfikator wątku, drugi parametr to nazwa wątku, a trzeci parametr to licznik, który określa, ile razy powinna zostać wykonana pętla while.
- thread2.start() Metoda start służy do rozpoczęcia wykonywania wątku. Wewnętrznie funkcja start() wywołuje metodę run() Twojej klasy.
- thread3.join() Metoda Join() blokuje wykonanie innego kodu i czeka aż zakończy się wątek, w którym została wywołana.
Jak już wiesz, wątki w tym samym procesie mają dostęp do pamięci i danych tego procesu. W rezultacie, jeśli więcej niż jeden wątek spróbuje jednocześnie zmienić lub uzyskać dostęp do danych, mogą pojawić się błędy.
W następnej sekcji zobaczysz różne rodzaje komplikacji, które mogą się pojawić, gdy wątki uzyskują dostęp do danych i sekcji krytycznej bez sprawdzania istniejących transakcji dostępu.
Blokady i warunki wyścigu
Zanim dowiesz się więcej o blokadach i wyścigach, pomocne będzie zrozumienie kilku podstawowych definicji związanych z programowaniem współbieżnym:
- Krytyczny fragment: Jest to fragment kodu, który uzyskuje dostęp do współdzielonych zmiennych lub je modyfikuje i musi być wykonywany jako transakcja atomowa.
- Zmiana kontekstu: Proces, w którym procesor zapisuje stan wątku przed przejściem z jednego zadania do drugiego, aby można było wznowić je później od tego samego punktu.
Zakleszczenia
Zakleszczenia są najbardziej obawianym problemem, z jakim spotykają się programiści piszący aplikacje współbieżne/wielowątkowe PythonNajlepszym sposobem na zrozumienie impasów jest wykorzystanie klasycznego przykładu problemu informatyki znanego jako Wyżywienie PhiloSophers Problem.
Problem dla filozofów-kulinarnych jest następujący:
Pięciu filozofów siedzi przy okrągłym stole z pięcioma talerzami spaghetti (rodzaj makaronu) i pięcioma widelcami, jak pokazano na diagramie.
Wyżywienie PhiloSophers Problem
Filozof w dowolnym momencie musi albo jeść, albo myśleć.
Co więcej, filozof musi wziąć dwa widelce sąsiadujące z nim (tj. lewy i prawy widelec), zanim będzie mógł zjeść spaghetti. Problem impasu pojawia się, gdy wszyscy pięciu filozofów bierze jednocześnie swoje prawe widelce.
Ponieważ każdy z filozofów ma jeden widelec, wszyscy będą czekać, aż inni odłożą widelec. W rezultacie żaden z nich nie będzie mógł zjeść spaghetti.
Podobnie w systemie współbieżnym, impas występuje, gdy różne wątki lub procesy (filozofowie) próbują pozyskać współdzielone zasoby systemowe (forki) w tym samym czasie. W rezultacie żaden z procesów nie ma szansy na wykonanie, ponieważ czekają na inny zasób przechowywany przez inny proces.
Warunki wyścigu
Wyścig to niepożądany stan programu, który występuje, gdy system wykonuje dwie lub więcej operacji jednocześnie. Rozważmy na przykład tę prostą pętlę for:
i=0; # a global variable for x in range(100): print(i) i+=1;
Jeśli tworzysz n Ze względu na liczbę wątków wykonujących ten kod jednocześnie, nie można określić wartości zmiennej i (współdzielonej przez wątki) po zakończeniu wykonywania programu. Dzieje się tak, ponieważ w rzeczywistym środowisku wielowątkowym wątki mogą się na siebie nakładać, a wartość zmiennej i pobrana i zmodyfikowana przez wątek może się zmienić w międzyczasie, gdy inny wątek uzyskuje do niej dostęp.
Oto dwie główne klasy problemów, które mogą wystąpić w środowisku wielowątkowym lub rozproszonym Python Aplikacja. W następnej sekcji dowiesz się, jak rozwiązać ten problem poprzez synchronizację wątków.
Synchronizujące wątki
Aby poradzić sobie z warunkami wyścigu, blokadami i innymi problemami związanymi z wątkami, moduł wątków zapewnia Zablokować obiekt. Pomysł polega na tym, że gdy wątek chce uzyskać dostęp do określonego zasobu, uzyskuje blokadę dla tego zasobu. Gdy wątek zablokuje określony zasób, żaden inny wątek nie będzie mógł uzyskać do niego dostępu, dopóki blokada nie zostanie zwolniona. W rezultacie zmiany w zasobie będą atomowe, a warunki wyścigu zostaną zażegnane.
Blokada to prymitywna synchronizacja niskiego poziomu implementowana przez _nitka moduł. W dowolnym momencie blokada może znajdować się w jednym z dwóch stanów: zamknięty or odblokowany. Obsługuje dwie metody:
- nabywać(): Gdy stan blokady jest odblokowany, wywołanie metody acquire() zmieni stan na zablokowany i zwróci wynik. Jeśli jednak stan jest zablokowany, wywołanie metody acquire() jest blokowane do momentu wywołania metody release() przez inny wątek.
- uwolnienie(): Metoda release() służy do ustawienia stanu na odblokowany, czyli do zwolnienia blokady. Można go wywołać dowolnym wątkiem, niekoniecznie tym, który uzyskał blokadę.
Oto przykład użycia blokad w aplikacjach. Uruchom IDLE i wpisz następujące polecenie:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Teraz naciśnij F5. Powinieneś zobaczyć wynik taki jak ten:
WYJAŚNIENIE KODU
- Tutaj po prostu tworzysz nową blokadę, wywołując metodę gwintowanie. Zablokuj() funkcja fabryczna. Wewnętrznie Lock() zwraca instancję najskuteczniejszej konkretnej klasy Lock obsługiwanej przez platformę.
- W pierwszej instrukcji blokadę uzyskujesz wywołując metodę nabycia(). Po przyznaniu blokady drukujesz „zamek zdobyty” do konsoli. Po zakończeniu wykonywania całego kodu, który ma zostać uruchomiony przez wątek, zwalniasz blokadę, wywołując metodę release().
Teoria jest dobra, ale skąd wiadomo, że blokada rzeczywiście zadziałała? Jeśli spojrzysz na wynik, zobaczysz, że każde z poleceń print drukuje dokładnie jeden wiersz na raz. Przypomnijmy, że we wcześniejszym przykładzie wyniki polecenia print były chaotyczne, ponieważ wiele wątków jednocześnie uzyskiwało dostęp do metody print(). W tym przypadku funkcja print jest wywoływana dopiero po nałożeniu blokady. Zatem wyniki są wyświetlane pojedynczo, wiersz po wierszu.
Oprócz zamków, Python obsługuje również inne mechanizmy obsługi synchronizacji wątków, wymienione poniżej:
- RZamki
- Semaphores
- Warunki
- Wydarzenia i
- Bariery
Globalna blokada tłumacza (i jak sobie z tym poradzić)
Zanim przejdziemy do szczegółów PythonGIL, zdefiniujmy kilka terminów, które będą przydatne w zrozumieniu nadchodzącej sekcji:
- Kod związany z procesorem: odnosi się to do dowolnego fragmentu kodu, który będzie bezpośrednio wykonywany przez procesor.
- Kod ograniczony wejściem/wyjściem: może to być dowolny kod, który uzyskuje dostęp do systemu plików poprzez system operacyjny.
- CPython: to jest odniesienie realizacja of Python i można go opisać jako interpreter napisany w C i Python (język programowania).
W czym jest GIL Python?
Globalna blokada tłumacza (GIL) in Python to blokada procesu lub mutex używany podczas obsługi procesów. Zapewnia ona dostęp jednego wątku do określonego zasobu w danym momencie, a także zapobiega jednoczesnemu używaniu obiektów i bajtkodów. Przynosi to korzyści programom jednowątkowym, zwiększając wydajność. GIL w Python jest bardzo prosty i łatwy do wdrożenia.
Blokadę można zastosować, aby mieć pewność, że w danym momencie tylko jeden wątek ma dostęp do określonego zasobu.
Jedna z cech Python polega na tym, że używa globalnej blokady dla każdego procesu interpretującego, co oznacza, że każdy proces traktuje Python interpreter sam w sobie jako zasób.
Na przykład załóżmy, że napisałeś Python Program, który wykorzystuje dwa wątki do wykonywania operacji procesora i wejścia/wyjścia. Po uruchomieniu tego programu dzieje się coś takiego:
- Python interpreter tworzy nowy proces i uruchamia wątki.
- Kiedy wątek-1 zacznie działać, najpierw uzyska GIL i zablokuje go.
- Jeśli wątek-2 chce teraz wykonać zadanie, będzie musiał poczekać na zwolnienie GIL, nawet jeśli inny procesor będzie wolny.
- Załóżmy teraz, że wątek 1 czeka na operację wejścia/wyjścia. W tym momencie zwolni GIL, a wątek 2 go przejmie.
- Jeśli po zakończeniu operacji we/wy wątek-1 chce się teraz wykonać, będzie musiał ponownie poczekać, aż GIL zostanie zwolniony przez wątek-2.
Z tego powodu tylko jeden wątek może uzyskać dostęp do interpretera w dowolnym momencie, co oznacza, że będzie wykonywany tylko jeden wątek Python kod w danym punkcie czasu.
W przypadku procesora jednordzeniowego jest to w porządku, ponieważ do obsługi wątków będzie on korzystał z podziału czasu (patrz pierwsza sekcja tego samouczka). Jednak w przypadku procesorów wielordzeniowych funkcja związana z obciążeniem procesora, wykonywana w wielu wątkach, będzie miała znaczny wpływ na wydajność programu, ponieważ w rzeczywistości nie będzie on wykorzystywał wszystkich dostępnych rdzeni jednocześnie.
Dlaczego GIL był potrzebny?
CPython Odśmiecacz wykorzystuje efektywną technikę zarządzania pamięcią znaną jako zliczanie referencji. Oto jak to działa: Każdy obiekt w Python Ma licznik odwołań, który zwiększa się po przypisaniu do nowej nazwy zmiennej lub dodaniu do kontenera (np. krotki, listy itp.). Podobnie, licznik odwołań zmniejsza się, gdy odwołanie wychodzi poza zakres lub gdy wywołana jest instrukcja del. Gdy licznik odwołań obiektu osiągnie 0, jest on usuwany przez program do usuwania śmieci, a przydzielona pamięć zostaje zwolniona.
Problem polega jednak na tym, że zmienna „liczba odwołań” jest podatna na wyścigi, podobnie jak każda inna zmienna globalna. Aby rozwiązać ten problem, twórcy Python Zdecydowano się na użycie globalnej blokady interpretera. Inną opcją było dodanie blokady do każdego obiektu, co skutkowałoby blokadami i zwiększonym narzutem wywołań acquire() i release().
Dlatego GIL stanowi istotne ograniczenie dla wielowątkowości Python programy wykonujące operacje obciążające procesor (co w praktyce czyni je jednowątkowymi). Jeśli chcesz wykorzystać w swojej aplikacji wiele rdzeni procesora, użyj wieloprocesowe zamiast tego moduł.








