Przykład wielowątkowości w Pythonie z Global Interpreter Lock (GIL)

Spisie treści:

Anonim

Język programowania Python pozwala na korzystanie z wieloprocesorowości lub wielowątkowości. W tym samouczku nauczysz się, jak pisać aplikacje wielowątkowe w Pythonie.

Co to jest wątek?

Wątek jest jednostką egzekucji w programowaniu współbieżnym. Wielowątkowość to technika, która pozwala procesorowi na wykonywanie wielu zadań jednego procesu w tym samym czasie. Wątki te mogą być wykonywane indywidualnie podczas współdzielenia zasobów procesowych.

Co to jest proces?

Proces to w zasadzie program w trakcie wykonywania. Kiedy uruchamiasz aplikację na swoim komputerze (np. Przeglądarkę lub edytor tekstu), system operacyjny tworzy proces.

Co to jest wielowątkowość w Pythonie?

Wielowątkowość w programowaniu w Pythonie jest dobrze znaną techniką, w której wiele wątków w procesie współdzieli swoją przestrzeń danych z głównym wątkiem, co sprawia, że ​​udostępnianie informacji i komunikacja w wątkach jest łatwa i wydajna. Wątki są lżejsze niż procesy. Wiele wątków może być wykonywanych indywidualnie podczas współdzielenia zasobów procesowych. Celem wielowątkowości jest jednoczesne uruchamianie wielu zadań i komórek funkcyjnych.

Co to jest przetwarzanie wieloprocesowe?

Wieloprocesorowość umożliwia jednoczesne uruchamianie wielu niepowiązanych ze sobą procesów. Te procesy nie dzielą się swoimi zasobami i nie komunikują się za pośrednictwem IPC.

Wielowątkowość w Pythonie a wielowątkowość

Aby zrozumieć procesy i wątki, rozważ następujący scenariusz: Plik .exe na komputerze to program. Po otwarciu system operacyjny ładuje go do pamięci, a procesor go wykonuje. Wystąpienie programu, które jest teraz uruchomione, nazywa się procesem.

Każdy proces będzie miał 2 podstawowe elementy:

  • Kod
  • Dane

Teraz proces może zawierać jedną lub więcej części podrzędnych zwanych wątkami. Zależy to od architektury systemu operacyjnego. Możesz myśleć o wątku jako o części procesu, która może być wykonywana oddzielnie przez system operacyjny.

Innymi słowy, jest to strumień instrukcji, które mogą być uruchamiane niezależnie przez system operacyjny. Wątki w ramach jednego procesu współużytkują dane tego procesu i są przeznaczone do współpracy w celu ułatwienia równoległości.

W tym samouczku dowiesz się,

  • Co to jest wątek?
  • Co to jest proces?
  • Co to jest wielowątkowość?
  • Co to jest przetwarzanie wieloprocesowe?
  • Wielowątkowość w Pythonie a wielowątkowość
  • Dlaczego warto korzystać z wielowątkowości?
  • Python MultiThreading
  • Moduły Threading i Threading
  • Moduł wątku
  • Moduł gwintowania
  • Zakleszczenia i warunki wyścigu
  • Synchronizacja wątków
  • Co to jest GIL?
  • Dlaczego GIL był potrzebny?

Dlaczego warto korzystać z wielowątkowości?

Wielowątkowość umożliwia podzielenie aplikacji na wiele pod-zadań i jednoczesne uruchamianie tych zadań. Prawidłowe korzystanie z wielowątkowości może poprawić szybkość, wydajność i renderowanie aplikacji.

Python MultiThreading

Python obsługuje konstrukcje zarówno dla przetwarzania wieloprocesowego, jak i wielowątkowego. W tym samouczku skupisz się przede wszystkim na wdrażaniu aplikacji wielowątkowych w języku Python. Istnieją dwa główne moduły, które mogą być używane do obsługi wątków w Pythonie:

  1. Gwint moduł i
  2. Gwintowania moduł

Jednak w Pythonie istnieje również coś, co nazywa się globalną blokadą interpretera (GIL). Nie pozwala na znaczny wzrost wydajności, a nawet może zmniejszyć wydajność niektórych aplikacji wielowątkowych. Dowiesz się wszystkiego na ten temat w kolejnych sekcjach tego samouczka.

Moduły Threading i Threading

Dwa moduły, o których dowiesz się w tym samouczku, to moduł wątków i moduł wątków .

Jednak moduł wątku od dawna jest przestarzały. Począwszy od Pythona 3, został oznaczony jako przestarzały i jest dostępny tylko jako __thread w celu zapewnienia zgodności z poprzednimi wersjami.

W przypadku aplikacji, które zamierzasz wdrożyć, należy użyć modułu wątkowości wyższego poziomu . Moduł wątku 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ę, aby rozpocząć kodowanie. Więc otwórz IDLE lub notatnik i wpisz:

import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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 zrobione poprawnie, oto wynik, który powinieneś zobaczyć:

Dowiesz się więcej o warunkach wyścigu i jak sobie z nimi radzić w kolejnych sekcjach

OBJAŚNIENIE KODU

  1. Te instrukcje importują czas i moduł wątków, które są używane do obsługi wykonywania i opóźniania wątków Pythona.
  2. Tutaj zdefiniowałeś funkcję o nazwie thread_test, która zostanie wywołana przez metodę start_new_thread . Funkcja wykonuje pętlę while dla czterech iteracji i wyświetla nazwę wątku, który ją wywołał. Po zakończeniu iteracji drukuje komunikat informujący, że wątek zakończył wykonywanie.
  3. To jest główna sekcja twojego programu. Tutaj po prostu wywołujesz metodę start_new_thread z funkcją thread_test jako argumentem.

    Spowoduje to utworzenie nowego wątku dla funkcji, którą przekazujesz jako argument i rozpoczęcie jej wykonywania. Zauważ, że możesz zastąpić to ( test _ wątku ) dowolną inną funkcją, którą chcesz uruchomić jako wątek.

Moduł gwintowania

Ten moduł to wysokopoziomowa implementacja wątków w Pythonie i de facto standard zarządzania aplikacjami wielowątkowymi. Zapewnia szeroki zakres funkcji w porównaniu z modułem gwintowym.

Struktura modułu Threading

Oto lista przydatnych funkcji zdefiniowanych w tym module:

Nazwa funkcji Opis
activeCount () Zwraca liczbę obiektów Thread, które nadal istnieją
currentThread () Zwraca bieżący obiekt klasy Thread.
wyliczać() Wyświetla wszystkie aktywne obiekty Thread.
isDaemon () Zwraca wartość true, jeśli wątek jest demonem.
żyje() Zwraca wartość true, jeśli wątek nadal żyje.
Metody klas wątków
początek() Rozpoczyna działanie wątku. Musi być wywołany tylko raz dla każdego wątku, ponieważ w przypadku wielokrotnego wywołania spowoduje zgłoszenie błędu w czasie wykonywania.
biegać() Ta metoda oznacza aktywność wątku i może zostać zastąpiona przez klasę, która rozszerza klasę Thread.
Przystąp() Blokuje wykonywanie innego kodu, dopóki wątek, w którym wywołano metodę join (), nie zostanie zakończony.

Backstory: The Thread Class

Przed rozpoczęciem kodowania programów wielowątkowych za pomocą modułu Threading, ważne jest, aby zapoznać się z klasą Thread. Klasa Thread jest podstawową klasą, która definiuje szablon i operacje wątku w Pythonie.

Najczęstszym sposobem tworzenia wielowątkowej aplikacji w języku Python jest zadeklarowanie klasy, która rozszerza klasę Thread i przesłania jej metodę run ().

Podsumowując, klasa Thread oznacza sekwencję kodu działającą w oddzielnym wątku sterowania.

Tak więc, pisząc aplikację wielowątkową, wykonasz następujące czynności:

  1. zdefiniuj klasę, która rozszerza klasę Thread
  2. Zastąp konstruktora __init__
  3. Zastąp metodę run ()

Po utworzeniu obiektu wątku można użyć metody start () do rozpoczęcia wykonywania tej czynności, a metody join () można użyć do zablokowania całego innego kodu do zakończenia bieżącej czynności.

Teraz spróbujmy użyć modułu wątkowania do zaimplementowania poprzedniego przykładu. Ponownie uruchom IDLE i wpisz:

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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:

OBJAŚNIENIE KODU

  1. Ta część jest taka sama, jak w poprzednim przykładzie. Tutaj importujesz moduł czasu i wątku, które są używane do obsługi wykonywania i opóźnień wątków Pythona.
  2. W tym fragmencie tworzysz klasę o nazwie Threadtester, która dziedziczy lub rozszerza klasę Thread modułu obsługi wątków. Jest to jeden z najpopularniejszych sposobów tworzenia wątków w Pythonie. Jednak w aplikacji należy przesłonić tylko konstruktora i metodę run () . Jak widać na powyższym przykładzie kodu, metoda __init__ (konstruktor) została nadpisana.

    Podobnie, nadpisałeś również metodę run () . Zawiera kod, który chcesz wykonać w wątku. W tym przykładzie wywołałeś funkcję thread_test ().

  3. To jest metoda thread_test (), która przyjmuje wartość i jako argument, zmniejsza ją o 1 przy każdej iteracji i wykonuje pętlę przez resztę kodu, aż i stanie się 0. W każdej iteracji wypisuje nazwę aktualnie wykonywanego wątku i śpi przez czekanie sekund (co jest również traktowane jako argument).
  4. thread1 = Threadtester (1, "Pierwszy wątek", 1)

    Tutaj tworzymy wątek i przekazujemy trzy parametry, które zadeklarowaliśmy w __init__. Pierwszy parametr to id wątku, drugi to nazwa wątku, a trzeci to licznik, który określa, ile razy powinna być uruchomiona pętla while.

  5. thread2.start ()

    Metoda start służy do uruchamiania wykonywania wątku. Wewnętrznie funkcja start () wywołuje metodę run () Twojej klasy.

  6. thread3.join ()

    Metoda join () blokuje wykonanie innego kodu i czeka na zakończenie wątku, w którym została wywołana.

Jak już wiesz, wątki, które są w tym samym procesie, mają dostęp do pamięci i danych tego procesu. W rezultacie, jeśli więcej niż jeden wątek próbuje jednocześnie zmienić lub uzyskać dostęp do danych, mogą wkradać 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.

Zakleszczenia i warunki wyścigu

Zanim dowiesz się o zakleszczeniach i warunkach wyścigu, 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ólnych zmiennych lub modyfikuje je i musi być wykonany jako niepodzielna transakcja.

  • Przełącznik kontekstu

    Jest to proces, który wykonuje procesor CPU, aby zapisać stan wątku przed przejściem z jednego zadania do drugiego, tak aby można go było później wznowić od tego samego punktu.

Impas

Zakleszczenia to najbardziej przerażający problem, z którym borykają się programiści podczas pisania aplikacji współbieżnych / wielowątkowych w Pythonie. Najlepszym sposobem na zrozumienie impasu jest wykorzystanie klasycznego problemu informatycznego znanego jako problem filozofów jedzenia.

Stwierdzenie problemu dla filozofów kulinarnych jest następujące:

Pięciu filozofów siedzi na okrągłym stole z pięcioma talerzami spaghetti (rodzaj makaronu) i pięcioma widelcami, jak pokazano na rysunku.

Problem kulinarnych filozofów

W dowolnym momencie filozof musi albo jeść, albo myśleć.

Ponadto filozof musi wziąć dwa sąsiadujące z nim widelce (tj. Widelec lewy i prawy), zanim będzie mógł zjeść spaghetti. Problem impasu pojawia się, gdy wszystkich pięciu filozofów jednocześnie podnosi właściwe rozwidlenia.

Ponieważ każdy z filozofów ma jeden widelec, wszyscy będą czekać, aż pozostali odłożą swój widelec. W efekcie żaden z nich nie będzie mógł zjeść spaghetti.

Podobnie w systemie współbieżnym do impasu dochodzi, gdy różne wątki lub procesy (filozofowie) próbują w tym samym czasie pozyskać współdzielone zasoby systemowe (rozwidlenia). W rezultacie żaden z procesów nie ma szansy na wykonanie, ponieważ czekają na inny zasób przechowywany przez inny proces.

Warunki wyścigu

Stan wyścigu to niepożądany stan programu, który występuje, gdy system wykonuje jednocześnie dwie lub więcej operacji. Na przykład rozważmy prostą pętlę for:

i=0; # a global variablefor x in range(100):print(i)i+=1;

Jeśli utworzysz n liczbę wątków, które jednocześnie uruchamiają ten kod, nie możesz określić wartości i (która jest współdzielona przez wątki) po zakończeniu wykonywania programu. Dzieje się tak, ponieważ w prawdziwym środowisku wielowątkowym wątki mogą się nakładać, a wartość i, która została pobrana i zmodyfikowana przez wątek, może zmieniać się w czasie, gdy inny wątek uzyskuje do niego dostęp.

Są to dwie główne klasy problemów, które mogą wystąpić w aplikacji wielowątkowej lub rozproszonej w języku Python. W następnej sekcji dowiesz się, jak rozwiązać ten problem poprzez synchronizację wątków.

Synchronizacja wątków

Aby poradzić sobie z warunkami wyścigu, zakleszczeniami i innymi problemami związanymi z wątkami, moduł wątkowości udostępnia obiekt Lock . Chodzi o to, ż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 zasobach będą atomowe, a warunki wyścigu zostaną zażegnane.

Blokada jest operacją podstawową synchronizacji niskiego poziomu zaimplementowaną przez moduł __thread . W dowolnym momencie zamek może znajdować się w jednym z 2 stanów: zablokowany lub otwarty. Obsługuje dwie metody:

  1. nabyć()

    Gdy stan blokady jest odblokowany, wywołanie metody acquiringu () zmieni stan na zablokowany i powrót. Jeśli jednak stan jest zablokowany, wywołanie metody acquiringu () jest blokowane do momentu wywołania metody release () przez inny wątek.

  2. wydanie()

    Metoda release () służy do ustawiania stanu na odblokowany, tj. 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:

import threadinglock = 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ć takie wyjście:

OBJAŚNIENIE KODU

  1. Tutaj po prostu tworzysz nową blokadę, wywołując funkcję fabryczną threading.Lock () . Wewnętrznie, Lock () zwraca instancję najbardziej efektywnej konkretnej klasy Lock, która jest obsługiwana przez platformę.
  2. W pierwszej instrukcji blokadę uzyskuje się, wywołując metodę nabycia (). Kiedy blokada jest przyznana, drukujesz na konsoli "blokadę uzyskaną" . Po zakończeniu wykonywania całego kodu, który ma być uruchamiany przez wątek, zwalniasz blokadę, wywołując metodę release ().

Teoria jest w porządku, ale skąd wiesz, że zamek naprawdę działał? Jeśli spojrzysz na wynik, zobaczysz, że każda z instrukcji print wypisuje dokładnie jeden wiersz na raz. Przypomnij sobie, że we wcześniejszym przykładzie dane wyjściowe z print były przypadkowe, ponieważ wiele wątków uzyskiwało dostęp do metody print () w tym samym czasie. Tutaj funkcja drukowania jest wywoływana dopiero po uzyskaniu blokady. Zatem wyjścia są wyświetlane pojedynczo i wiersz po wierszu.

Oprócz blokad, python obsługuje również inne mechanizmy do obsługi synchronizacji wątków, wymienione poniżej:

  1. RLocks
  2. Semafory
  3. Warunki
  4. Wydarzenia i
  5. Bariery

Global Interpreter Lock (i jak sobie z tym radzić)

Zanim przejdziemy do szczegółów GIL-a Pythona, zdefiniujmy kilka terminów, które będą przydatne w zrozumieniu nadchodzącej sekcji:

  1. Kod związany z procesorem: odnosi się do dowolnego fragmentu kodu, który zostanie bezpośrednio wykonany przez procesor.
  2. Kod związany z we / wy: może to być dowolny kod, który uzyskuje dostęp do systemu plików za pośrednictwem systemu operacyjnego
  3. CPython: jest referencyjną implementacją Pythona i można go opisać jako interpreter napisany w C i Pythonie (język programowania).

Co to jest GIL w Pythonie?

Global Interpreter Lock (GIL) w Pythonie to blokada procesu lub mutex używany podczas obsługi procesów. Daje pewność, że jeden wątek może uzyskać dostęp do określonego zasobu na raz, a także zapobiega jednoczesnemu użyciu obiektów i kodów bajtowych. Jest to korzystne dla programów jednowątkowych w postaci wzrostu wydajności. GIL w Pythonie jest bardzo prosty i łatwy do wdrożenia.

Można użyć blokady, aby upewnić się, że tylko jeden wątek ma dostęp do określonego zasobu w danym momencie.

Jedną z cech Pythona jest to, że używa globalnej blokady na każdym procesie interpretera, co oznacza, że ​​każdy proces traktuje sam interpreter Pythona jako zasób.

Na przykład załóżmy, że napisałeś program w języku Python, który używa dwóch wątków do wykonywania operacji procesora i operacji we / wy. Po uruchomieniu tego programu dzieje się tak:

  1. Interpreter Pythona tworzy nowy proces i odradza wątki
  2. Kiedy Thread-1 zacznie działać, najpierw pobierze GIL i zablokuje go.
  3. Jeśli wątek-2 chce wykonać teraz, będzie musiał poczekać na zwolnienie GIL, nawet jeśli inny procesor jest wolny.
  4. Teraz załóżmy, że wątek-1 czeka na operację we / wy. W tym momencie zwolni GIL, a wątek-2 go uzyska.
  5. Po zakończeniu operacji we / wy, jeśli wątek-1 chce wykonać teraz, 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 ​​w danym momencie będzie tylko jeden wątek wykonujący kod Pythona.

Jest to w porządku w przypadku jednordzeniowego procesora, ponieważ do obsługi wątków będzie używany podział czasu (zobacz pierwszą sekcję tego samouczka). Jednak w przypadku procesorów wielordzeniowych funkcja związana z procesorem wykonywana na wielu wątkach będzie miała znaczny wpływ na wydajność programu, ponieważ w rzeczywistości nie będzie on używał wszystkich dostępnych rdzeni w tym samym czasie.

Dlaczego GIL był potrzebny?

Moduł wyrzucania elementów bezużytecznych w CPython wykorzystuje wydajną technikę zarządzania pamięcią znaną jako liczenie odwołań. Oto jak to działa: każdy obiekt w Pythonie ma liczbę odwołań, która jest zwiększana, gdy jest przypisany do nowej nazwy zmiennej lub dodany do kontenera (np. Krotki, listy itp.). Podobnie liczba odwołań jest zmniejszana, gdy odwołanie wykracza poza zakres lub gdy wywoływana jest instrukcja del. Gdy liczba odwołań do obiektu osiągnie 0, jest on usuwany z pamięci, a przydzielona pamięć jest zwalniana.

Problem polega jednak na tym, że zmienna licznika odniesień jest podatna na warunki wyścigu, jak każda inna zmienna globalna. Aby rozwiązać ten problem, twórcy Pythona zdecydowali się użyć globalnej blokady interpretera. Inną opcją było dodanie blokady do każdego obiektu, co spowodowałoby zakleszczenie i zwiększone obciążenie wywołane wywołaniami pozyskiwania () i release ().

Dlatego GIL jest znaczącym ograniczeniem dla wielowątkowych programów w języku Python wykonujących ciężkie operacje związane z procesorem (skutecznie czyniąc je jednowątkowymi). Jeśli chcesz korzystać z wielu rdzeni procesora w swojej aplikacji, użyj zamiast tego modułu wieloprocesorowego .

Podsumowanie

  • Python obsługuje 2 moduły do ​​wielowątkowości:
    1. Moduł __thread : zapewnia niskopoziomową implementację wątków i jest przestarzały.
    2. moduł wątkowości : zapewnia implementację wysokiego poziomu dla wielowątkowości i jest obecnym standardem.
  • Aby utworzyć wątek za pomocą modułu wątków, musisz wykonać następujące czynności:
    1. Utwórz klasę, która rozszerza klasę Thread .
    2. Zastąp jego konstruktor (__init__).
    3. Zastąp jego metodę run () .
    4. Utwórz obiekt tej klasy.
  • Wątek można wykonać, wywołując metodę start () .
  • Metoda join () może służyć do blokowania innych wątków, dopóki ten wątek (ten, w którym wywołano łączenie) nie zakończy wykonywania.
  • Stan wyścigu występuje, gdy wiele wątków uzyskuje dostęp do udostępnionego zasobu lub modyfikuje go w tym samym czasie.
  • Można tego uniknąć, synchronizując wątki.
  • Python obsługuje 6 sposobów synchronizacji wątków:
    1. Zamki
    2. RLocks
    3. Semafory
    4. Warunki
    5. Wydarzenia i
    6. Bariery
  • Blokady pozwalają tylko konkretnemu wątkowi, który uzyskał blokadę, wejść do sekcji krytycznej.
  • Blokada ma 2 podstawowe metody:
    1. Pozyskiwanie () : Ustawia stan blokady na zablokowany. Jeśli zostanie wywołany na zablokowanym obiekcie, blokuje się do momentu zwolnienia zasobu.
    2. release () : Ustawia stan blokady na odblokowany i powraca. Jeśli zostanie wywołany na odblokowanym obiekcie, zwraca wartość false.
  • Globalna blokada interpretera to mechanizm, dzięki któremu tylko 1 proces interpretera języka CPython może być wykonywany na raz.
  • Został użyty w celu ułatwienia funkcji zliczania odniesień w garbage collector w CPythons.
  • Aby tworzyć aplikacje w języku Python z dużymi operacjami związanymi z procesorem, należy użyć modułu wieloprocesorowego.