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ątekjak 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 threading

co 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 ⚠️.

Użycie konstruktora wątku z modułu "threading" z przypisaniem funkcji bez parametrów w języku Python

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ę 👍.

Użycie konstruktora wątku z modułu "threading" z przypisaniem funkcji bez parametrów w języku Python

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 🔥!

Metoda "start" obiektu wątku z modułu "threading" w języku Python

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 time

A 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 👇:

  1. minimalna liczba zakresu liczbowego,
  2. maksymalna liczba zakresu liczbowego,
  3. 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 👇:

  1. wyznacza liczbę plików do zapisania na jeden wątek (tu wstawiłem 10),
  2. 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.),
  3. 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.),
  4. wyznacza opóźnienie w sekundach (pewna stała pomnożona przez aktualny numer wątku, aby pokazać różnorodność),
  5. tworzy obiekt wątku wprowadzając nazwę funkcji i parametry wyznaczone w punktach 2-4,
  6. 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ż 😉!

PODOBNE ARTYKUŁY