Otwieram kolejny artykuł o Pythonie, w którym zaprezentuję Ci podstawy programowania wielowątkowego 😱! W języku Python istnieje moduł "threading", który pozwala na pisanie programów operujących na wielu wątkach jednocześnie ⏩! Opowiem Ci czym w dużym skrócie jest wątek, jak go utworzyć przy pomocy wspomnianego modułu oraz jaki to przynosi skutek w trakcie działania programu 🔥! Także zapraszam!
MODUŁ "THREADING" W JĘZYKU PYTHON UTWORZY DLA CIEBIE WIELE WĄTKÓW!
Zanim przejdziemy do poznawania samego modułu, musimy wpierw dowiedzieć się o kolejnej porcji wiedzy teoretycznej 🧠! Poznajmy pojęcie wątku 👀!
CZYM JEST WĄTEK W PROGRAMOWANIU?
W programowaniu, wątek jest wydzielonym fragmentem kodu wykonującym się równolegle z pozostałą częścią programu ⌚. Jak piszemy kod do programu, to normalnie instrukcje wykonują się jedna po drugiej, tak jak je widzimy w edytorze. Gdy dodajemy w programie osobny wątek, to z chwilą jego uruchomienia powstaje jakby "klon" tego samego ciągu instrukcji, które mają być wykonane przez ten wątek i co może brzmieć zaskakująco, on wykonuje się w tym samym czasie, w jakim obecnie działa program w sposób "standardowy" 💥!
Zaletą stosowania wątków jest znaczne przyspieszenie działania programu, ponieważ blok instrukcji przekazany wątkowi wykonuje się równolegle z programem, dzięki czemu coś wymagającego wykonania wielu skomplikowanych obliczeniowo operacji, może zostać przekazanych kolejnym rdzeniom procesora (jeśli nasz procesor jest wielordzeniowy) ❤️.
Wątki stanowią już jedno z bardziej zaawansowanych zagadnień w programowaniu, jako że na początku trudno jest zrozumieć samo ich działanie, a co dopiero mówić o samodzielnym wprowadzeniu ich do programu i wiedzy w jaki sposób to zrobić 😳.
Teraz gdy wiemy już o co się rozchodzi w tytule artykułu, przejdźmy do poznania modułu, który umożliwi nam utworzenie i uruchomienie wątku 😉!
CZYM JEST "THREADING" W JĘZYKU PYTHON?
"threading" oznacza w Pythonie moduł służący do programowania wielowątkowego, za pomocą którego możemy tworzyć wątki, uruchamiać je i konfigurować im instrukcje jakie mają się wykonać 🔁. W ten sposób jesteśmy w stanie napisać taki program, w którym wykonuje się wiele bloków kodu naraz 💪!
Przejdźmy zatem do tego, w jaki sposób skorzystać z modułu "threading" 📜!
JAK SKORZYSTAĆ Z MODUŁU "THREADING" W JĘZYKU PYTHON?
Moduł "threading" w Pythonie wymaga zaimportowania, takiego jak przy całej reszcie modułów ↩️. Piszemy u samej góry naszego skryptu, poniższą linijkę 👇:
import threadingco oznacza instrukcję importu modułu 👍. Od tej pory, wszystkie wartości i funkcje jakie znajdują się w tym module, stoją przed nami otworem 🔓. Gdybyś chciał(a) zaimportować jedynie wybrany fragment modułu (czy to konkretną wartość, czy funkcję), to lepiej wtedy skorzystać z następującego zapisu 👇:
from threading import [wartość/metoda występująca w module]Tylko miej świadomość, że inaczej odwołujesz się do tych wartości i/lub funkcji używając samego słowa "import", a inaczej z połączeniem z "from" ⚠️. Detale odnośnie różnic w zapisie zostaną zaprezentowane w odrębnych artykułach ℹ️. Cały dalszy ciąg materiału będzie opierać się na pierwszym zapisie 1️⃣.
Możemy przejść w końcu do konkretnego zagadnienia: jak utworzyć wątek w Pythonie 🚀!
JAK STWORZYĆ WĄTEK W JĘZYKU PYTHON?
Używając modułu "threading", wystarczy że skorzystamy z klasy "Thread" i utworzymy nowy obiekt wątku pisząc instrukcję następującej postaci 👇:
my_thread = threading.Thread(target=[nazwa funkcji z instrukcjami do wykonania przez wątek], args=[argumenty pozycyjne przekazywane do funkcji], kwargs=[argumenty nazwane przekazywane do funkcji])Wytłumaczmy sobie najpierw samą postać, abyśmy rozumieli parametry jakie przyjmuje obiekt wątku 🙂.
Powyższy konstruktor klasy "Thread" (metoda tworząca obiekt ℹ️) korzysta z następujących argumentów nazwanych 👇:
- "target" = nazwa funkcji jaką ma się zająć wątek w momencie jego uruchomienia (nie piszemy wywołania, tylko samą nazwę!),
- "args" = lista lub krotka z wartościami parametrów zgodnie z kolejnością określoną w funkcji, którą wymieniliśmy w poprzednim punkcie,
- "kwargs" = słownik z parami klucz-wartość jako przyporządkowana wartość do nazwy parametru zgodnie z wprowadzaniem wartości stosując argumenty nazwane.
Argument nazwany oznacza wartość parametru przypisywaną funkcji poprzedzoną jego nazwą zgodną z zapisem funkcji (więcej o tym będzie w osobnym artykule) ℹ️. Oczywiście rozwinę teraz wszystkie postaci parametrów, tak abyś miał(a) wiedzę na temat każdego z nich 😉!
JAK PRZEKAZAĆ WĄTKOWI FUNKCJĘ W JĘZYKU PYTHON?
Parametr "target" zawarty w konstruktorze klasy "Thread" służy do przekazywania nazwy funkcji, czyli czym ma się zająć wątek, gdy zostanie on uruchomiony ✅. Wątki w Pythonie operują na funkcjach, ponieważ pozwalają one na wydzielanie instrukcji na mniejsze części 👍.
Gdy mamy dowolną funkcję 👇:
def print_message():
print('Wykonuję instrukcje przez wątek')to możemy ją wskazać podczas tworzenia wątku używając parametru "target" 👇:
my_thread = threading.Thread(target=print_message)Tyle w zupełności wystarczy, aby wątkowi poprawnie przekazać co ma robić 👍. "target" oczekuje samej nazwy funkcji, a nie wywołania (wywoływać będzie później), także pomijamy wstawianie nawiasów okrągłych ⚠️.
Aby przekazać wątkowi funkcję do wywołania w momencie jego uruchomienia, korzystamy z argumentu nazwanego "target" i wskazujemy nazwę funkcji jaka ma zostać wykonana.
Zwróć uwagę, że ta funkcja nie przyjmuje żadnych parametrów 👀. Możemy jak najbardziej podstawić funkcję do wątku przyjmującą jakiekolwiek parametry, aczkolwiek wtedy sam parametr "target" nie wystarczy 🚫!
JAK PRZEKAZAĆ WĄTKOWI PARAMETRY DO FUNKCJI W JĘZYKU PYTHON?
Aby prawidłowo przekazać parametry do funkcji jaką ma wykonać wątek, w konstruktorze klasy "Thread", poza parametrem "target", należy jeszcze dodać kolejny argument nazwany i on zależy od tego, czy chcesz wprowadzać parametry zgodnie z kolejnością, czy niestandardowo (za pomocą argumentów nazwanych) 👇:
- "args", gdy chcesz wprowadzać parametry według kolejności ustalonej w nagłówku funkcji,
- "kwargs" (skrót od "keyword arguments" ℹ️), gdy potrzebujesz wprowadzić parametry w nieregularnej kolejności, używając argumentów nazwanych.
Tak wygląda przykład funkcji przyjmującej 2 parametry 👇:
def print_numbers(minimum_number, maximum_number):
for number in range(minimum_number, maximum_number + 1):
print(number)i mając taką funkcję, możemy przekazać wątkowi do wywołania wartości parametrów zgodnie z kolejnością, korzystając z "args" 👇:
my_thread = threading.Thread(target=print_numbers, args=(3, 6))To da taki efekt, że pierwszy parametr otrzyma wartość 3, a drugi 6, czyli dokładnie tak, jak wskazuje kolejność 👍.
A to drugi sposób zapisu przy użyciu argumentów nazwanych, stosując "kwargs" 👇:
my_thread = threading.Thread(target=print_numbers, kwargs={'minimum_number': 3, 'maximum_number': 6})To da identyczny efekt 🙂. Można też połączyć jedno z drugim i częściowo skorzystać z "args", i częściowo z "kwargs" 🙂.
my_thread = threading.Thread(target=print_numbers, args=(3,), kwargs={'maximum_number': 6})Jeżeli w części parametru "args" umieszczasz tylko jedną wartość, to muszę Cię przestrzec przed małą pułapką składniową jaka występuje w Pythonie 💥! Koniecznie zwróć uwagę na "wystający" przecinek po jedynym parametrze "args" 👀! To nie jest żadna literówka, tylko należy go tam umieścić jeśli w środku ma się znajdować tylko jeden parametr. Gdy ma być ich więcej, to nie trzeba tego robić ☑️. Teraz pytanie "o co chodzi?".
"args" musi przyjmować sekwencję, aby doszło do prawidłowego utworzenia wątku. Gdy wprowadzimy taki zapis 👇:
(3)to oznacza on pojedynczą liczbę całkowitą (bez ułamka) 😮! Python po prostu opuści nawiasy i zostanie tylko liczba 3. Natomiast kiedy wpiszemy w taki sposób 👇:
(3,)będzie to oznaczać utworzenie krotki jednoelementowej 😱! Interpreter Pythona wykrywa krotkę po przecinku, nie po nawiasach (które są symbolem krotki jako typu danych) ⚠️! Dlatego trzeba pamiętać, że ten przecinek na końcu nie jest "na wyrost", tylko aby Python interpretował to jako krotkę 🔴!
Oczywiście nie muszę uświadamiać, aby uważać przy określaniu parametrów zarówno przy jednym, jak i drugim zapisie, aby się nie pomylić przy liczbie parametrów (używając "args") czy wprowadzaniu ich nazw (używając "kwargs"). W obu przypadkach, pomyłka grozi awarią programu, zgłoszeniem błędu i natychmiastowym zakończeniem programu (jeśli wyjątek nie jest przechwycony) ❌!
Dodam jeszcze na koniec, że możesz bez żadnych problemów korzystać z wartości domyślnych wstawianych w miejsce parametrów oczekiwanych przez funkcję 👍.
Jeżeli wątek ma wywołać funkcję z parametrami, należy użyć parametrów "args" (jeśli chcemy wprowadzić parametry według podanej kolejności) bądź "kwargs" (jeśli chcemy skorzystać z argumentów nazwanych). Można użyć jednego i drugiego zapisu jednocześnie.
Mamy wyjaśnione jak skonfigurować wątek do wykonania określonego zadania ✔️. Teraz jak go uruchomić ⏩!
JAK URUCHOMIĆ WĄTEK W JĘZYKU PYTHON?
Gdy mamy obiekt wątku utworzony za pomocą modułu "threading" w języku Python, jedyne co wystarczy do jego uruchomienia, to wywołanie metody "start" 👇:
my_thread.start()To spowoduje natychmiastowe rozpoczęcie wykonywania wątku i funkcji, którą mu przydzielono ▶️. Przypominam, że uruchomienie wątku przypomina "sklonowanie" tego samego bloku kodu do wykonania i od tej pory będzie się on wykonywać równolegle z resztą programu 🔥!
Uruchamianie wątku odbywa się poprzez wywołanie metody "start".
Na zakończenie zdobytej wiedzy, krótki praktyczny przykład działania wątków 😉!
PRZYKŁAD UŻYCIA WĄTKU W JĘZYKU PYTHON
Na fajne ukazanie wykonywania wielu instrukcji w jednym czasie przygotowałem program, w którym tworzymy N wątków mających podłączoną funkcję odpowiedzialną za zapis określonej liczby plików tekstowych do folderu ze skryptem 😁. Aby zasymulować niewielkie obciążenie wykonywaną operacją, dodamy sobie opóźnienie poprzez użycie metody "sleep" z modułu "time" 💤. Także na sam początek zaimportuj sobie moduł "time" przy pomocy tej instrukcji 👇:
import timeA teraz prezentacja kodu źródłowego 🎥!
FUNKCJA DLA WĄTKU
To jest funkcja jaka zostanie przypisana każdemu z wątków 👇:
def save_text_files(minimum_number, maximum_number, delay_in_seconds):
for number in range(minimum_number, maximum_number + 1):
with open('plik_' + str(number) + '.txt', 'w'):
pass
time.sleep(delay_in_seconds)Funkcja przyjmuje 3 następujące parametry 👇:
- minimalna liczba zakresu liczbowego,
- maksymalna liczba zakresu liczbowego,
- opóźnienie w sekundach.
Wewnątrz funkcji korzystamy z pętli "for", aby wykonać wiele razy tę samą operację stosując zakres liczbowy (typ danych "range") 👍. Dwa pierwsze parametry służą do wyznaczenia granic tego przedziału, czyli od jakiej liczby do jakiej, mają zostać wykonane dane instrukcje ↔️.
Spójrz na zwiększenie maksymalnego zakresu o 1 w drugim parametrze "range" 👀! On jest konieczny, aby podana górna granica została uwzględniona, ponieważ "range" jej nie obejmuje (nazywa się to "exclusive end" ℹ️). Porównując to do przedziału liczbowego z matematyki, przyjąłby postać: ⚠️. Stąd należy dodać "jedynkę" do parametru, aby mieć pewność, że zakres obejmie wartość podaną w drugim parametrze ✅.
W środku pętli "for" tworzymy sobie instrukcję "with", która automatycznie przeprowadzi otwarcie i zamknięcie pliku po zakończeniu ostatniej instrukcji w bloku, których akurat w tym przypadku nie ma (jest tylko słowo kluczowe "pass") 🙃. Nie potrzebujemy żadnego zapisywania do pliku, więc w gruncie rzeczy to spowoduje tylko utworzenie nowego pliku, otwarcie i zamknięcie go 😄. Aby wstawić daną liczbę w nazwę pliku, należy ją skonwertować na łańcuch znaków - stąd korzystamy z "str" 💡.
Po instrukcji "with" mamy wywołanie metody "sleep" z modułu "time", które na krótką chwilę wstrzymuje dalsze wykonywanie instrukcji, aczkolwiek w tym przypadku zadziała inaczej, niż "tradycyjnie" 😮! "Zamrożenie" będzie dotyczyć jedynie tego wątku, a nie całego programu 💥!
To koniec wyjaśniania tej funkcji 😉.
TWORZENIE I URUCHAMIANIE WĄTKÓW
A oto kod odpowiedzialny za utworzenie wątków i ich uruchamianie 👇:
threads_instances = []
number_of_threads = 3
for number in range(1, number_of_threads + 1):
number_of_files_to_save = 10
minimum_number = 1 + number_of_files_to_save*(number - 1)
maximum_number = number_of_files_to_save*number
delay_in_seconds = 0.5*number
thread = threading.Thread(target=save_text_files, args=(minimum_number, maximum_number, delay_in_seconds))
threads_instances.append(thread)
for thread in threads_instances:
thread.start()Tutaj również mamy sporo instrukcji, więc wyjaśniam idąc od góry 📣!
Na samej górze znajdują się 2 zmienne: lista do przechowywania obiektów wątków i liczba całkowita (bez ułamka) określająca ile wątków ma zostać utworzonych ✅. Następnie zostaje wstawiona pętla "for" operująca na zakresie od 1 do podanej liczby wątków z tym, że tu występuje zwiększenie o 1 do góry, ponieważ "range" nie uwzględnia podanej górnej granicy (jest ona określana jako "exclusive end" ℹ️).
Dla każdej liczby od 1 do N, pętla wykonuje następujące kroki 👇:
- wyznacza liczbę plików do zapisania na jeden wątek (tu wstawiłem 10),
- wyznacza minimalną liczbę pliku (powyższe wyrażenie oznacza "start" od liczby 1, a potem co każdy wątek zwiększamy początkową liczbę o 10, czyli wtedy jest 11, 21 itd.),
- wyznacza maksymalną liczbę pliku (mnożymy aktualny numer wątku przez liczbę plików do zapisania na jeden wątek, czyli będzie 10, 20 itd.),
- wyznacza opóźnienie w sekundach (pewna stała pomnożona przez aktualny numer wątku, aby pokazać różnorodność),
- tworzy obiekt wątku wprowadzając nazwę funkcji i parametry wyznaczone w punktach 2-4,
- dodaje obiekt wątku do listy.
Tak wygląda działanie tej większej pętli "for" 😅. A ta druga ma na celu przejście po wszystkich elementach listy i uruchomienie wątku wywołaniem metody "start" 😊. Na tym się kończy cały kod źródłowy 🏁!
Gdy uruchomimy sobie cały ten program, to dostrzeżemy ciekawe zjawisko 👀! Pliki tekstowe nagle zaczną pojawiać się w tym samym katalogu, w którym znajduje się skrypt 😮! O wiele bardziej zaskakujące będzie to, iż te pliki będą się pojawiać nieregularnie, czyli nie od pierwszego do ostatniego, tylko zgodnie z przebiegiem każdego z wątków 😱!
Wszystkie 3 wątki zaczną wykonywać swoje zadanie nie interesując się tym, co robi sąsiad obok 😅. W ten sposób powstał program rozdzielający większe zadanie na kilka osobnych wątków sprawiając, że cała operacja pójdzie znacznie szybciej, ponieważ każdy z nich będzie wykonywać mniejszą "porcję" całości ❤️! To oczywiście jest naciągane i zapis plików tekstowych można spokojnie zrealizować jedną pętlą "for", natomiast chciałem pokazać samą istotę wydzielania instrukcji na mniejsze części 🙂. Na tym właśnie polega programowanie wielowątkowe 🚀!
Można to było napisać z użyciem dużo mniejszej liczby wierszy, jednak wolałem to rozbić na pojedyncze zmienne, aby wszystko było widoczne i łatwiejsze do własnej analizy 👍. Spróbuj sam(a) teraz poeksperymentować wstawiając inną liczbę wątków bądź plików przypadających na jeden wątek 🙂.
Pod sam koniec tego artykułu, ostatnia rzecz, na jaką chciałem zwrócić uwagę 🔔.
CO WARTO WIEDZIEĆ O WĄTKACH W JĘZYKU PYTHON?
Powyższy przykład pokazuje działanie wielu wątków naraz, natomiast nie wyczerpuje wszystkich jego możliwości 😳. Na przykład zwracanie wartości przez funkcję. Nie jest to możliwe do wykonania przez wątki i gdy będziesz tego potrzebować, musisz skorzystać z pakietu "concurrent", bo tylko on wspiera zwracanie wyniku przez funkcję na wyjściu 💡.
Druga rzecz na którą chciałem zwrócić uwagę, to modyfikowanie wartości zmiennych przez wątki 🔥. Ten temat robi się jeszcze bardziej zawiły gdy wchodzi modyfikacja i odczyt wartości, gdyż wątki korzystają z tej samej puli danych! A ponieważ każdy z nich wykonuje swoje czynności w tym samym czasie i nie patrzy na to, co robi sąsiedni wątek, to jesteśmy narażeni na sytuację, w której wartości będą błędne, bo na przykład wątek A odczyta daną wartość wcześniej, zanim zmodyfikuje ją wątek B i nieszczęście gotowe 😐.
Także gdyby wśród czytelników trafił się ktoś, kto chce poszerzyć swoją wiedzę w kierunku programowania wielowątkowego, niech dobrze przestudiuje zagadnienie zapewnienia spójności danych pomiędzy wątkami (określa się to też "synchronizacją")❗. Jest to niezmiernie istotne, szczególnie gdy planujesz programowanie w oparciu o wiele wątków operujących na danych w obie strony (zapis i odczyt), z których korzysta cała reszta 💥!
To wszystko, co można uznać za wprowadzenie do wielowątkowości 🙂. Zdaję sobie sprawę, że nawet po dokładnym przeczytaniu tego materiału dalej możesz nie rozumieć tego tematu, jako że jest już dużo trudniejszy od wielu innych zagadnień, a i tak to nie wyczerpuje wszystkich rozdziałów 😅. Także nie przejmuj się, poczytaj, poćwicz, poeksperymentuj 🧪. Trening czyni mistrza, w programowaniu też 😉!