Last modified: February 19, 2020
This article is written in: 🇵🇱
Wątki to jednostki wykonawcze procesu, które umożliwiają równoległe wykonanie różnych fragmentów kodu w obrębie jednego programu. Zastosowanie wątków może znacząco przyspieszyć działanie aplikacji, zwłaszcza gdy mamy do czynienia z operacjami blokującymi, takimi jak łączenie się z zewnętrznymi serwerami, wczytywanie dużych plików czy obliczenia numeryczne.
Wątki działają jako lekkie podprocesy wewnątrz głównego procesu programu. Każdy wątek może wykonywać niezależnie fragmenty kodu, współdzieląc zasoby (np. pamięć) z innymi wątkami tego samego procesu. W Pythonie, do zarządzania wątkami używamy modułu threading
.
Kluczowe aspekty działania wątków:
Tworzenie i uruchamianie wątku:
Thread
.start
.Wykonywanie kodu w wątku:
start
, wątek rozpoczyna wykonywanie przypisanej funkcji.Zakończenie wątku:
join
pozwala głównemu programowi czekać na zakończenie wątku, zanim przejdzie dalej.Aby korzystać z wątków w Pythonie, potrzebujemy modułu threading
.
import threading
def moja_funkcja():
print("Rozpoczynam pracę w wątku:", threading.current_thread().name)
# Tworzenie nowego wątku
watek = threading.Thread(target=moja_funkcja, name="Watek-1")
# Uruchamianie wątku
watek.start()
# Oczekiwanie na zakończenie wątku
watek.join()
Kilka ważnych uwag na temat wątków w Pythonie:
join
pozwala głównemu wątkowi czekać na zakończenie wątków potomnych.Tworzenie własnego wątku w Pythonie jest stosunkowo proste dzięki modułowi threading
. Głównym podejściem jest dziedziczenie po klasie Thread
i nadpisanie metody run
, która definiuje działania, które mają być wykonane w obrębie tego wątku. Aby uruchomić wątek, po utworzeniu jego instancji, wywołujemy metodę start
.
Poniżej przedstawiono przykład tworzenia prostego wątku:
import threading
class MojWatek(threading.Thread):
def run(self):
# kod, który zostanie wykonany w wątku
print("Wątek uruchomiony")
watek = MojWatek()
watek.start()
Jeżeli chcemy przekazać argumenty do naszego wątku, możemy to zrobić poprzez konstruktor klasy. Pamiętajmy, aby w konstruktorze wywołać konstruktor klasy nadrzędnej przy użyciu super()
.
import threading
class MojWatek(threading.Thread):
def __init__(self, argument):
super().__init__()
self.argument = argument
def run(self):
# kod, który zostanie wykonany w wątku
print(f"Wątek uruchomiony z argumentem: {self.argument}")
watek = MojWatek("Hello World")
watek.start()
Ważne jest, aby pamiętać o potencjalnych problemach związanych z wielowątkowością, takich jak konkurencyjny dostęp do wspólnych zasobów czy potencjalne sytuacje wyścigowe. W przypadku potrzeby synchronizacji wątków warto skorzystać z mechanizmów dostarczanych przez moduł threading
, takich jak Lock
czy Semaphore
.
Kontrolowanie i zatrzymywanie wątków w Pythonie może być nieco skomplikowane, ale dzięki narzędziom dostarczanym przez moduł threading
jest to możliwe do osiągnięcia.
join()
Głównym sposobem oczekiwania na zakończenie wątku jest użycie metody Thread.join()
. Blokuje ona wywołujący wątek do momentu zakończenia wątku, na którym została wywołana.
watki = [watek1, watek2, watek3]
for watek in watki:
watek.join()
Można również określić maksymalny czas oczekiwania za pomocą parametru timeout
w metodzie join()
. Jeśli po tym czasie wątek nie zakończył pracy, główny wątek zostanie wznowiony.
Event
Zmienna Event
w module threading
umożliwia komunikację między wątkami. Wątek może oczekiwać na sygnał (metoda wait()
) i inny wątek może wysłać ten sygnał (metoda set()
).
import threading
stop_event = threading.Event()
def worker_thread():
while not stop_event.is_set():
# wątek wykonuje swoje zadania
do_some_work()
# po otrzymaniu sygnału wątek jest zatrzymywany
stop_event.clear()
# ...
# główny wątek chce zatrzymać worker_thread
stop_event.set()
Uwagi:
stop_event.is_set()
) w nieskończoność, gdyż mogą one powodować niepotrzebne obciążenie procesora.W wielowątkowych aplikacjach często występuje potrzeba korzystania ze współdzielonych zasobów, takich jak zmienne czy struktury danych. Jednak równoczesny dostęp wielu wątków do tych zasobów może prowadzić do nieprzewidywalnych i niepożądanych skutków, takich jak sytuacje wyścigowe. Aby uniknąć tych problemów, konieczne jest użycie mechanizmów synchronizacji.
Obiekt Lock
z modułu threading
pozwala na zapewnienie, że tylko jeden wątek na raz może wykonywać określony fragment kodu.
Przykład:
import threading
# Globalna zmienna dostępna dla wielu wątków
zmienna_globalna = 0
# Obiekt Zamek do synchronizacji dostępu do zmienna_globalna
blokada = threading.Lock()
def funkcja_watek1():
global zmienna_globalna
for _ in range(100):
with blokada: # Zastosowanie kontekstu zamiast manualnego pobierania i zwalniania blokady
zmienna_globalna += 1
def funkcja_watek2():
global zmienna_globalna
for _ in range(100):
with blokada:
zmienna_globalna -= 1
# Tworzenie i uruchamianie wątków
watek1 = threading.Thread(target=funkcja_watek1)
watek2 = threading.Thread(target=funkcja_watek2)
watek1.start()
watek2.start()
# Czekanie na zakończenie wątków
watek1.join()
watek2.join()
# Wyświetlenie wyniku
print(zmienna_globalna)
W powyższym przykładzie:
- Zamiast korzystać z manualnego pobierania (acquire()
) i zwalniania blokady (release()
), użyto konstrukcji with blokada:
, która jest bardziej elegancka i automatycznie zwalnia blokadę nawet w przypadku wystąpienia błędów.
- Zmniejszono ryzyko wystąpienia zjawiska wyścigu i zapewniono, że zmienne globalne są aktualizowane w sposób kontrolowany.
Semafory to kolejny mechanizm synchronizacji, który może być użyty, gdy potrzebujemy kontrolować dostęp do zasobu przez określoną liczbę wątków jednocześnie.
Przykład użycia semafora:
import threading
import time
# Semafor z limitem 2 wątków
semafora = threading.Semaphore(2)
def funkcja_watek(numer):
with semafora:
print(f"Wątek {numer} rozpoczął pracę")
time.sleep(2)
print(f"Wątek {numer} zakończył pracę")
# Tworzenie i uruchamianie wątków
watki = [threading.Thread(target=funkcja_watek, args=(i,)) for i in range(5)]
for watek in watki:
watek.start()
for watek in watki:
watek.join()
W powyższym przykładzie:
with semafora:
.GIL, czyli Global Interpreter Lock, to mechanizm obecny w standardowej implementacji Pythona (CPython), który zapewnia, że w danej chwili tylko jeden wątek może wykonywać kod bajtowy Pythona. GIL został wprowadzony, aby rozwiązać problemy związane z zarządzaniem pamięcią w kontekście wielowątkowości, zapewniając jednocześnie efektywność jednowątkowego kodu.
multiprocessing
), każdy proces posiada własną instancję interpretera i własny GIL, dzięki czemu może działać równolegle.Przykład z sumowaniem elementów listy jest dość prosty i niekoniecznie pokazuje faktyczne ograniczenia GIL. Bardziej zaawansowane operacje, takie jak obliczenia CPU, mogą naprawdę odczuwać negatywne skutki GIL w wielowątkowym środowisku.
import threading
def suma_listy(liczby):
suma = sum(liczby) # Uproszczony sposób sumowania
print(suma)
# Utworzenie wątków
watki = [
threading.Thread(target=suma_listy, args=([1, 2, 3],)),
threading.Thread(target=suma_listy, args=([4, 5, 6],)),
threading.Thread(target=suma_listy, args=([7, 8, 9],))
]
# Uruchomienie wątków
for watek in watki:
watek.start()
# Oczekiwanie na zakończenie wątków
for watek in watki:
watek.join()
W powyższym przykładzie wszystkie wątki mogą wykonywać operacje sumowania, ale ze względu na GIL, tylko jeden wątek na raz wykonuje kod Pythona. W rzeczywistości, sumowanie w każdym wątku odbywa się sekwencyjnie, a nie równolegle.
W przypadkach, gdy chcemy maksymalnie wykorzystać wielordzeniowość procesora w Pythonie, warto rozważyć użycie procesów zamiast wątków lub użycie innych narzędzi:
multiprocessing
pozwala tworzyć procesy, z których każdy ma własną instancję Pythona i własny GIL, umożliwiając rzeczywistą równoległość.
from multiprocessing import Process
def suma_listy(liczby):
suma = sum(liczby)
print(suma)
# Utworzenie procesów
procesy = [
Process(target=suma_listy, args=([1, 2, 3],)),
Process(target=suma_listy, args=([4, 5, 6],)),
Process(target=suma_listy, args=([7, 8, 9],))
]
# Uruchomienie procesów
for proces in procesy:
proces.start()
# Oczekiwanie na zakończenie procesów
for proces in procesy:
proces.join()
Użycie bibliotek specjalizujących się w współbieżności: Biblioteki takie jak Dask
czy Joblib
mogą uprościć równoległe przetwarzanie danych i zarządzanie zadaniami w Pythonie.
Użycie Cython: Cython pozwala na kompilację kodu Pythona do kodu C, co może znacznie przyspieszyć wykonywanie obliczeń intensywnych pod względem CPU i umożliwić lepsze wykorzystanie wielordzeniowości.
Ostateczna uwaga: Gdy chcemy przyspieszyć operacje w Pythonie, warto również rozważyć zastosowanie odpowiednich algorytmów i struktur danych, które mogą wpłynąć na wydajność kodu niezależnie od użycia wątków czy procesów.