Wątki Intercommunication
W prawdziwym życiu, jeśli zespół ludzi pracuje nad wspólnym zadaniem, powinna istnieć między nimi komunikacja w celu prawidłowego wykonania zadania. Ta sama analogia dotyczy również wątków. W programowaniu, aby skrócić idealny czas procesora, tworzymy wiele wątków i przypisujemy różne zadania podrzędne do każdego wątku. Dlatego musi istnieć możliwość komunikacji i powinni oni współdziałać ze sobą, aby zakończyć pracę w zsynchronizowany sposób.
Rozważ następujące ważne punkty związane z komunikacją między wątkami -
No performance gain - Jeśli nie możemy osiągnąć prawidłowej komunikacji między wątkami i procesami, korzyści wydajnościowe wynikające z współbieżności i równoległości są bezużyteczne.
Accomplish task properly - Bez odpowiedniego mechanizmu komunikacji między wątkami przydzielone zadanie nie może zostać poprawnie zakończone.
More efficient than inter-process communication - Komunikacja między wątkami jest wydajniejsza i łatwiejsza w użyciu niż komunikacja między procesami, ponieważ wszystkie wątki w procesie mają tę samą przestrzeń adresową i nie muszą używać pamięci współdzielonej.
Struktury danych Python do bezpiecznej wątkowo komunikacji
W przypadku kodu wielowątkowego pojawia się problem z przekazywaniem informacji z jednego wątku do drugiego. Standardowe prymitywy komunikacyjne nie rozwiązują tego problemu. Dlatego musimy zaimplementować własny obiekt złożony, aby udostępniać obiekty między wątkami, aby komunikacja była bezpieczna dla wątków. Poniżej znajduje się kilka struktur danych, które zapewniają bezpieczną komunikację wątkowo po wprowadzeniu w nich pewnych zmian -
Zestawy
Aby używać struktury danych zestawu w sposób bezpieczny dla wątków, musimy rozszerzyć klasę zestawu, aby zaimplementować własny mechanizm blokujący.
Przykład
Oto przykład rozszerzenia klasy w Pythonie -
class extend_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(extend_class, self).__init__(*args, **kwargs)
def add(self, elem):
self._lock.acquire()
try:
super(extend_class, self).add(elem)
finally:
self._lock.release()
def delete(self, elem):
self._lock.acquire()
try:
super(extend_class, self).delete(elem)
finally:
self._lock.release()
W powyższym przykładzie obiekt klasy o nazwie extend_class została zdefiniowana, która jest dalej dziedziczona z Pythona set class. Obiekt lock jest tworzony w konstruktorze tej klasy. Teraz są dwie funkcje -add() i delete(). Te funkcje są zdefiniowane i są bezpieczne dla wątków. Oboje polegają nasuper funkcjonalność klasy z jednym kluczowym wyjątkiem.
Dekorator
Jest to kolejna kluczowa metoda komunikacji bezpiecznej dla wątków, polegająca na użyciu dekoratorów.
Przykład
Rozważmy przykład Pythona, który pokazuje, jak używać dekoratorów & mminus;
def lock_decorator(method):
def new_deco_method(self, *args, **kwargs):
with self._lock:
return method(self, *args, **kwargs)
return new_deco_method
class Decorator_class(set):
def __init__(self, *args, **kwargs):
self._lock = Lock()
super(Decorator_class, self).__init__(*args, **kwargs)
@lock_decorator
def add(self, *args, **kwargs):
return super(Decorator_class, self).add(elem)
@lock_decorator
def delete(self, *args, **kwargs):
return super(Decorator_class, self).delete(elem)
W powyższym przykładzie zdefiniowano metodę dekoratora o nazwie lock_decorator, która jest dalej dziedziczona z klasy metod Pythona. Następnie w konstruktorze tej klasy tworzony jest obiekt blokady. Teraz mamy dwie funkcje - add () i delete (). Te funkcje są zdefiniowane i są bezpieczne dla wątków. Oba polegają na najwyższej klasy funkcjonalności z jednym kluczowym wyjątkiem.
Listy
Struktura danych listy jest bezpieczna dla wątków, szybka i łatwa do tymczasowego przechowywania w pamięci. W Cpython GIL chroni przed jednoczesnym dostępem do nich. Jak dowiedzieliśmy się, że listy są bezpieczne dla wątków, ale co z zawartymi w nich danymi. W rzeczywistości dane listy nie są chronione. Na przykład,L.append(x)nie gwarantuje zwrócenia oczekiwanego wyniku, jeśli inny wątek próbuje zrobić to samo. To dlatego, że chociażappend() jest operacją niepodzielną i bezpieczną dla wątków, ale drugi wątek próbuje jednocześnie modyfikować dane listy, dlatego na wyjściu widać skutki uboczne warunków wyścigu.
Aby rozwiązać tego rodzaju problem i bezpiecznie zmodyfikować dane, musimy zaimplementować odpowiedni mechanizm blokujący, który dodatkowo gwarantuje, że wiele wątków nie może potencjalnie znaleźć się w warunkach wyścigu. Aby zaimplementować odpowiedni mechanizm blokujący, możemy rozszerzyć klasę, tak jak to zrobiliśmy w poprzednich przykładach.
Niektóre inne niepodzielne operacje na listach są następujące -
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
Tutaj -
- L, L1, L2 to listy
- D, D1, D2 to dykty
- x, y to obiekty
- i, j są ints
Kolejki
Jeśli dane listy nie są chronione, możemy ponieść konsekwencje. Możemy otrzymać lub usunąć błędne dane dotyczące warunków wyścigu. Dlatego zaleca się stosowanie struktury danych kolejki. Rzeczywistym przykładem kolejki może być jednopasmowa droga jednokierunkowa, na której pojazd wjeżdża pierwszy, zjeżdża jako pierwszy. Więcej rzeczywistych przykładów można zobaczyć w kolejkach przy kasach biletowych i na przystankach autobusowych.
Kolejki są domyślnie bezpieczną wątkowo strukturą danych i nie musimy się martwić o implementację złożonego mechanizmu blokowania. Python zapewnia nam
Rodzaje kolejek
W tej sekcji omówimy różne typy kolejek. Python udostępnia trzy opcje kolejek do użycia z<queue> moduł -
- Normalne kolejki (FIFO, pierwsze weszło, pierwsze wyszło)
- LIFO, Last in First Out
- Priority
Dowiemy się o różnych kolejkach w kolejnych sekcjach.
Normalne kolejki (FIFO, pierwsze weszło, pierwsze wyszło)
Jest to najczęściej używana implementacja kolejki oferowana przez Pythona. W tym mechanizmie kolejkowania ten, kto będzie pierwszy, otrzyma usługę jako pierwszy. FIFO jest również nazywane normalnymi kolejkami. Kolejki FIFO można przedstawić w następujący sposób -
Implementacja kolejki FIFO w języku Python
W Pythonie kolejkę FIFO można zaimplementować zarówno z pojedynczym wątkiem, jak iz wielowątkowością.
Kolejka FIFO z pojedynczym wątkiem
Aby zaimplementować kolejkę FIFO z pojedynczym wątkiem, plik Queueclass zaimplementuje podstawowy kontener „pierwszy na wejściu, pierwszy na wyjściu”. Elementy zostaną dodane do jednego „końca” sekwencji za pomocąput()i usunięte z drugiego końca za pomocą get().
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki FIFO z pojedynczym wątkiem -
import queue
q = queue.Queue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end = " ")
Wynik
item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7
Dane wyjściowe pokazują, że powyższy program używa pojedynczego wątku do zilustrowania, że elementy są usuwane z kolejki w tej samej kolejności, w jakiej są wstawiane.
Kolejka FIFO z wieloma wątkami
Aby zaimplementować FIFO z wieloma wątkami, musimy zdefiniować funkcję myqueue (), która jest rozszerzana z modułu queue. Działanie metod get () i put () jest takie samo, jak omówiono powyżej przy implementacji kolejki FIFO z pojedynczym wątkiem. Następnie, aby był wielowątkowy, musimy zadeklarować i utworzyć instancję wątków. Wątki te będą zużywać kolejkę w sposób FIFO.
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki FIFO z wieloma wątkami
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.Queue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue
LIFO, kolejka Last in First Out
Ta kolejka używa zupełnie odwrotnej analogii niż kolejki FIFO (First in First Out). W tym mechanizmie kolejkowania ten, kto jest ostatni, otrzyma usługę jako pierwszy. Jest to podobne do implementacji struktury danych stosu. Kolejki LIFO okazują się przydatne podczas implementacji wyszukiwania w głąb, takich jak algorytmy sztucznej inteligencji.
Implementacja kolejki LIFO w Pythonie
W Pythonie kolejkę LIFO można zaimplementować zarówno z pojedynczym wątkiem, jak iz wielowątkowością.
Kolejka LIFO z pojedynczym wątkiem
Aby zaimplementować kolejkę LIFO z pojedynczym wątkiem, plik Queue class zaimplementuje podstawowy kontener „ostatni na wejściu, pierwszy na wyjściu” przy użyciu struktury Queue.LifoQueue. Teraz dzwonięput(), elementy są dodawane w głowicy pojemnika i usuwane z głowicy również podczas używania get().
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki LIFO z pojedynczym wątkiem -
import queue
q = queue.LifoQueue()
for i in range(8):
q.put("item-" + str(i))
while not q.empty():
print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0
Dane wyjściowe pokazują, że powyższy program używa pojedynczego wątku do zilustrowania, że elementy są usuwane z kolejki w odwrotnej kolejności, w jakiej są wstawiane.
Kolejka LIFO z wieloma wątkami
Implementacja jest podobna do implementacji kolejek FIFO z wieloma wątkami. Jedyna różnica polega na tym, że musimy użyć rozszerzeniaQueue klasa, która zaimplementuje podstawowy kontener „ostatni na wejściu, pierwszy na wyjściu” przy użyciu struktury Queue.LifoQueue.
Przykład
Poniżej znajduje się program w Pythonie do implementacji kolejki LIFO z wieloma wątkami -
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
q.put(i)
threads = []
for i in range(4):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue
Kolejka priorytetowa
W kolejkach FIFO i LIFO kolejność pozycji jest powiązana z kolejnością wstawiania. Jednak w wielu przypadkach priorytet jest ważniejszy niż kolejność wstawiania. Rozważmy przykład z prawdziwego świata. Załóżmy, że ochrona na lotnisku sprawdza osoby należące do różnych kategorii. Osoby z VVIP, personel linii lotniczych, urzędnik celny, kategorie mogą być sprawdzane na zasadzie pierwszeństwa zamiast sprawdzania na podstawie przylotu, tak jak w przypadku zwykłych ludzi.
Innym ważnym aspektem, który należy wziąć pod uwagę w przypadku kolejki priorytetowej, jest sposób opracowania harmonogramu zadań. Jednym z typowych projektów jest obsługa większości zadań agenta na podstawie priorytetów w kolejce. Ta struktura danych może służyć do pobierania elementów z kolejki na podstawie ich wartości priorytetu.
Implementacja kolejki priorytetów w Pythonie
W Pythonie kolejka priorytetowa może być implementowana zarówno z pojedynczym wątkiem, jak i wielowątkowością.
Kolejka priorytetowa z jednym wątkiem
Aby zaimplementować kolejkę priorytetową z pojedynczym wątkiem, plik Queue class zaimplementuje zadanie na kontenerze priorytetowym przy użyciu struktury Queue.Kolejka priorytetowa. Teraz dzwonięput(), elementy są dodawane z wartością, przy której najniższa wartość będzie miała najwyższy priorytet, a zatem zostanie pobrana jako pierwsza przy użyciu get().
Przykład
Rozważmy następujący program w Pythonie do implementacji kolejki Priority z pojedynczym wątkiem -
import queue as Q
p_queue = Q.PriorityQueue()
p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))
while not p_queue.empty():
item = p_queue.get()
print('%s - %s' % item)
Wynik
1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important
Na powyższym wyjściu widzimy, że kolejka przechowała pozycje w oparciu o priorytet - wartość bez priorytetu ma wysoki priorytet.
Kolejka priorytetowa z wieloma wątkami
Implementacja jest podobna do implementacji kolejek FIFO i LIFO z wieloma wątkami. Jedyna różnica polega na tym, że musimy użyć rozszerzeniaQueue klasa do inicjowania priorytetu przy użyciu struktury Queue.PriorityQueue. Inna różnica dotyczy sposobu generowania kolejki. W poniższym przykładzie zostanie wygenerowany z dwoma identycznymi zestawami danych.
Przykład
Poniższy program w Pythonie pomaga w implementacji kolejki priorytetowej z wieloma wątkami -
import threading
import queue
import random
import time
def myqueue(queue):
while not queue.empty():
item = queue.get()
if item is None:
break
print("{} removed {} from the queue".format(threading.current_thread(), item))
queue.task_done()
time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
q.put(i,1)
for i in range(5):
q.put(i,1)
threads = []
for i in range(2):
thread = threading.Thread(target=myqueue, args=(q,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Wynik
<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue