2.1 Moduł threading
Wielowątkowość w Pythonie to sposób wykonywania kilku wątków (threads)
jednocześnie, co pozwala efektywniej wykorzystać zasoby procesora, zwłaszcza w operacjach wejścia-wyjścia lub innych zadaniach, które mogą być wykonywane równolegle.
Podstawowe pojęcia wielowątkowości w Pythonie:
Wątek — to najmniejsza jednostka wykonania, która może działać równolegle z innymi wątkami w jednym procesie. Wszystkie wątki w jednym procesie dzielą wspólną pamięć, co pozwala na wymianę danych między nimi.
Proces — to egzemplarz programu działający w systemie operacyjnym, z własnym obszarem adresowym i zasobami. W przeciwieństwie do wątków, procesy są izolowane od siebie i wymieniają dane poprzez interprocesową komunikację (IPC)
.
GIL
— to mechanizm w interpreterze Pythona, który zapobiega równoczesnemu wykonywaniu kilku wątków Pythona. GIL
zapewnia bezpieczeństwo wykonywania kodu Pythona, ale ogranicza wydajność wielowątkowych programów na procesorach wielordzeniowych.
Ważne! Pamiętaj, że z powodu Global Interpreter Lock (GIL) wielowątkowość w Pythonie może nie zapewnić znaczącego wzrostu wydajności dla zadań intensywnie obliczeniowych, ponieważ GIL zapobiega równoczesnemu wykonywaniu kilku wątków Pythona na procesorach wielordzeniowych.
Moduł threading
Moduł threading
w Pythonie oferuje wysokopoziomowy interfejs do pracy z wątkami. Pozwala tworzyć i zarządzać wątkami, synchronizować je i organizować współdziałanie między nimi. Przyjrzyjmy się kluczowym komponentom i funkcjom tego modułu bardziej szczegółowo.
Główne komponenty modułu threading
Elementy do pracy z wątkami:
-
Thread
— główna klasa do tworzenia i zarządzania wątkami. -
Timer
— timer do wykonania funkcji po upływie określonego czasu. -
ThreadLocal
— pozwala tworzyć dane lokalne dla wątku.
Mechanizm synchronizacji wątków:
-
Lock
— prymityw synchronizacji zapobiegający równoczesnemu dostępowi do wspólnych zasobów. -
Condition
— zmienna warunkowa do bardziej złożonej synchronizacji wątków. Event
— prymityw do powiadamiania między wątkami.-
Semaphore
— prymityw do ograniczania liczby wątków, które mogą jednocześnie wykonywać określony obszar. -
Barrier
— synchronizuje określoną liczbę wątków, blokując je, dopóki wszystkie nie osiągną bariery.
Poniżej opowiem o 3 klasach do pracy z wątkami, natomiast mechanizm synchronizacji wątków w najbliższym czasie nie będzie ci potrzebny.
2.2 Klasa Thread
Klasa Thread
— główna klasa do tworzenia i zarządzania wątkami. Ma 4 podstawowe metody:
start()
: Rozpoczyna wykonanie wątku.-
join()
: Aktualny wątek wstrzymuje się i czeka na zakończenie uruchomionego wątku. is_alive()
: ZwracaTrue
, jeśli wątek jest wykonywany.-
run()
: Metoda zawierająca kod, który będzie wykonywany w wątku. Należy ją przesłonić przy dziedziczeniu od klasyThread
.
Wszystko jest znacznie prostsze, niż się wydaje — przykład użycia klasy Thread
.
Uruchomienie prostego wątku
import threading
def worker():
print("Worker thread is running")
# Tworzenie nowego wątku
t = threading.Thread(target=worker) #utworzono nowy obiekt wątku
t.start() #Wystartowano wątek
t.join() # Oczekiwanie na zakończenie wątku
print("Main thread is finished")
Po wywołaniu metody start, funkcja worker rozpocznie swoje wykonanie. A właściwie jej wątek zostanie dodany do listy aktywnych wątków.
Używanie argumentów
import threading
def worker(number, text):
print(f"Worker {number}: {text}")
# Tworzenie nowego wątku z argumentami
t = threading.Thread(target=worker, args=(1, "Hello"))
t.start()
t.join()
Aby przekazać parametry do nowego wątku, wystarczy podać je w formie krotki i przypisać do parametru args
. Przy wywoływaniu funkcji, która została wskazana w target
, parametry zostaną przekazane automatycznie.
Przesłanianie metody run
import threading
class MyThread(threading.Thread):
def run(self):
print("Custom thread is running")
# Tworzenie i uruchamianie wątku
t = MyThread()
t.start()
t.join()
Istnieją dwa sposoby wskazania funkcji, od której należy rozpocząć wykonanie nowego wątku — można ją przekazać przez parametr target
przy tworzeniu obiektu Thread
, lub dziedziczyć od klasy Thread
i przesłonić funkcję run
. Oba sposoby są legalne i używane regularnie.
2.3 Klasa Timer
Klasa Timer
w module threading
służy do uruchamiania funkcji po określonym czasie. Ta klasa jest przydatna do wykonywania odroczonych zadań w środowisku wielowątkowym.
Timer tworzy się i inicjalizuje za pomocą funkcji, którą należy wywołać, oraz czasu opóźnienia w sekundach.
- Metoda
start()
uruchamia timer, który odlicza określony interwał czasu, a następnie wywołuje wskazaną funkcję. - Metoda
cancel()
pozwala zatrzymać timer, jeśli jeszcze się nie uruchomił. To przydatne, aby zapobiec wykonaniu funkcji, jeśli timer już nie jest potrzebny.
Przykłady użycia:
Uruchomienie funkcji z opóźnieniem
W tym przykładzie funkcja hello
zostanie wywołana 5 sekund po uruchomieniu timera.
import threading
def hello():
print("Hello, world!")
# Tworzenie timera, który wywoła funkcję hello po 5 sekundach
t = threading.Timer(5.0, hello)
t.start() # Uruchomienie timera
Zatrzymanie timera przed wykonaniem
Tutaj timer zostanie zatrzymany, zanim funkcja hello
zdąży się wykonać, więc nic nie zostanie wypisane.
import threading
def hello():
print("Hello, world!")
# Tworzenie timera
t = threading.Timer(5.0, hello)
t.start() # Uruchomienie timera
# Zatrzymanie timera przed wykonaniem
t.cancel()
Timer z argumentami
W tym przykładzie timer wywoła funkcję greet
po 3 sekundach i przekaże do niej argument "Alice"
.
import threading
def greet(name):
print(f"Hello, {name}!")
# Tworzenie timera z argumentami
t = threading.Timer(3.0, greet, args=["Alice"])
t.start()
Klasa Timer
jest wygodna do planowania wykonywania zadań po określonym czasie. Jednocześnie timery nie gwarantują absolutnie dokładnego czasu wykonania, ponieważ zależy to od obciążenia systemu i pracy planisty wątków.
2.4 Klasa ThreadLocal
Klasa ThreadLocal
jest przeznaczona do tworzenia wątków, które mają swoje własne lokalne dane. To jest przydatne w aplikacjach wielowątkowych, kiedy każdy wątek musi mieć swoją wersję danych, aby uniknąć konfliktów i problemów synchronizacji.
Każdy wątek korzystający z ThreadLocal
ma swoje własne niezależne kopie danych. Dane zapisane w obiekcie ThreadLocal
są unikalne dla każdego wątku i nie są współdzielone z innymi wątkami. To przydatne do przechowywania danych, które są używane tylko w kontekście jednego wątku, takich jak bieżący użytkownik w aplikacji webowej czy bieżące połączenie z bazą danych.
Przykłady użycia:
Podstawowe użycie
W tym przykładzie każdy wątek przypisuje swoją nazwę do lokalnej zmiennej value
i wyświetla ją. Wartość value
jest unikalna dla każdego wątku.
import threading
# Tworzenie obiektu ThreadLocal
local_data = threading.local()
def process_data():
# Przypisanie wartości do lokalnej zmiennej wątku
local_data.value = threading.current_thread().name
# Dostęp do lokalnej zmiennej wątku
print(f'Value in {threading.current_thread().name}: {local_data.value}')
threads = []
for i in range(5):
t = threading.Thread(target=process_data)
threads.append(t)
t.start()
for t in threads:
t.join()
Przechowywanie danych użytkownika w aplikacji webowej
W tym przykładzie każdy wątek przetwarza żądanie dla swojego użytkownika. Wartość user_data.user
jest unikalna dla każdego wątku.
import threading
# Tworzenie obiektu ThreadLocal
user_data = threading.local()
def process_request(user):
# Przypisanie wartości do lokalnej zmiennej wątku
user_data.user = user
handle_request()
def handle_request():
# Dostęp do lokalnej zmiennej wątku
print(f'Handling request for user: {user_data.user}')
threads = []
users = ['Alice', 'Bob', 'Charlie']
for user in users:
t = threading.Thread(target=process_request, args=(user,))
threads.append(t)
t.start()
for t in threads:
t.join()
To były 3 najprzydatniejsze klasy biblioteki threading
. Prawdopodobnie będziesz ich używać w swojej pracy, a reszty klas – raczej nie. Teraz wszyscy przechodzą na funkcje asynchroniczne i bibliotekę asyncio
. O niej właśnie będziemy rozmawiać przez najbliższy czas.
GO TO FULL VERSION