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