Oto wyjaśnienie i przykład, jak to osiągnąć. Daj mi znać, jeśli są części, które nie są jasne.
Gist ze źródłem
uniwersalny
Inicjalizacja:
Indeksy wątków są stosowane w sposób przyrostowy atomowy. Jest to zarządzane za pomocą AtomicInteger
nazwanego nextIndex
. Indeksy te są przypisywane do wątków poprzez ThreadLocal
instancję, która inicjuje się, pobierając następny indeks nextIndex
i zwiększając go. Dzieje się tak za pierwszym razem, gdy indeks każdego wątku jest pobierany po raz pierwszy. Utworzono A w ThreadLocal
celu śledzenia ostatniej sekwencji utworzonej przez ten wątek. Zainicjowano 0. Sekwencyjne odniesienie do obiektu fabryki jest przekazywane i przechowywane. AtomicReferenceArray
Tworzone są dwa wystąpienia wielkości n
. Obiekt ogona jest przypisany do każdego odniesienia, po zainicjowaniu go ze stanu początkowego dostarczonego przez Sequential
fabrykę. n
to maksymalna dozwolona liczba wątków. Każdy element w tych tablicach „należy” do odpowiedniego indeksu wątku.
Zastosuj metodę:
Jest to metoda, która wykonuje interesującą pracę. Wykonuje następujące czynności:
- Utwórz nowy węzeł dla tego wywołania: moje
- Ustaw ten nowy węzeł w tablicy zapowiedzi przy indeksie bieżącego wątku
Następnie rozpoczyna się pętla sekwencjonowania. Będzie on działał, dopóki bieżące wywołanie nie zostanie zsekwencjonowane:
- znajdź węzeł w tablicy zapowiedzi, używając sekwencji ostatniego węzła utworzonego przez ten wątek. Więcej na ten temat później.
- jeśli węzeł zostanie znaleziony w kroku 2, nie jest jeszcze zsekwencjonowany, kontynuuj go, w przeciwnym razie po prostu skup się na bieżącym wywołaniu. Spróbuje to pomóc tylko jednemu innemu węzłowi na każde wywołanie.
- Niezależnie od tego, który węzeł został wybrany w kroku 3, próbuj sekwencjonować go po ostatnim zsekwencjonowanym węźle (inne wątki mogą się zakłócać). Niezależnie od sukcesu ustaw bieżące odniesienie nagłówka wątku do sekwencji zwróconej przez
decideNext()
Kluczem do opisanej powyżej pętli zagnieżdżonej jest decideNext()
metoda. Aby to zrozumieć, musimy spojrzeć na klasę Node.
Klasa węzłowa
Ta klasa określa węzły na liście podwójnie połączonej. W tej klasie nie ma dużo akcji. Większość metod to proste metody wyszukiwania, które powinny być dość oczywiste.
metoda ogona
zwraca to specjalną instancję węzła o sekwencji 0. Po prostu działa jako symbol zastępczy, dopóki wywołanie nie zastąpi jej.
Właściwości i inicjalizacja
seq
: numer sekwencyjny, zainicjowany na -1 (co oznacza, że nie jest porządkowany)
invocation
: wartość wywołania apply()
. Ustawiony na budowę.
next
: AtomicReference
dla linku do przesyłania dalej. raz przypisany, nigdy nie zostanie zmieniony
previous
: AtomicReference
dla linku wstecznego przypisanego podczas sekwencjonowania i wyczyszczonego przeztruncate()
Wybierz następny
Ta metoda jest tylko jedna w węźle z nietrywialną logiką. W skrócie, węzeł jest oferowany jako kandydat na następny węzeł na połączonej liście. compareAndSet()
Metoda sprawdzi, czy jest to odniesienie jest zerowy, a jeśli tak, to ustawić odniesienie do kandydata. Jeśli odwołanie jest już ustawione, nie robi nic. Ta operacja ma charakter atomowy, więc jeśli dwóch kandydatów zostanie zaoferowanych w tym samym momencie, zostanie wybrany tylko jeden. To gwarantuje, że tylko jeden węzeł zostanie wybrany jako następny. Jeśli wybrany zostanie węzeł kandydujący, jego kolejność zostanie ustawiona na następną wartość, a jego poprzednie łącze zostanie ustawione na ten węzeł.
Powrót do klasy uniwersalnej Zastosuj metodę ...
Po wywołaniu decideNext()
ostatniego zsekwencjonowanego węzła (po sprawdzeniu) albo naszym węzłem, albo węzłem z announce
tablicy, istnieją dwa możliwe przypadki: 1. Węzeł został pomyślnie zsekwencjonowany 2. Niektóre inne wątki uprzedziły ten wątek.
Następnym krokiem jest sprawdzenie, czy węzeł utworzony dla tego wywołania. Może się tak zdarzyć, ponieważ ten wątek z powodzeniem zsekwencjonował go lub inny wątek podniósł go z announce
tablicy i zsekwencjonował dla nas. Jeśli nie został zsekwencjonowany, proces jest powtarzany. W przeciwnym razie wywołanie kończy się przez wyczyszczenie tablicy announce dla indeksu tego wątku i zwrócenie wartości wynikowej wywołania. Tablica zapowiedzi została wyczyszczona, aby zagwarantować, że nie pozostały żadne odniesienia do węzła, które uniemożliwiłyby zbieranie śmieci i dlatego wszystkie węzły na liście połączonej od tego momentu pozostają aktywne na stosie.
Oceń metodę
Teraz, gdy węzeł wywołania został pomyślnie zsekwencjonowany, wywołanie musi zostać ocenione. Aby to zrobić, pierwszym krokiem jest upewnienie się, że wywołania poprzedzające to zostały ocenione. Jeśli tego nie zrobią, ten wątek nie będzie czekał, ale wykona tę pracę natychmiast.
Metoda surePrior
ensurePrior()
Sposób to działa sprawdzając poprzedni węzeł w połączonej listy. Jeśli jego stan nie jest ustawiony, poprzedni węzeł zostanie oceniony. Węzeł, że jest to rekurencyjne. Jeśli węzeł poprzedzający poprzedni węzeł nie został oceniony, wywoła funkcję oceny dla tego węzła i tak dalej.
Teraz, gdy wiadomo, że poprzedni węzeł ma stan, możemy go ocenić. Ostatni węzeł jest pobierany i przypisywany do zmiennej lokalnej. Jeśli to odwołanie ma wartość null, oznacza to, że jakiś inny wątek uprzedził ten i ocenił już ten węzeł; ustawienie to stan. W przeciwnym razie stan poprzedniego węzła jest przekazywany do Sequential
metody stosowania obiektu wraz z wywołaniem tego węzła. Zwrócony stan jest ustawiany w węźle i truncate()
wywoływana jest metoda, usuwając łącze zwrotne z węzła, ponieważ nie jest już potrzebne.
Metoda MoveForward
Metoda przesuwania do przodu spróbuje przenieść wszystkie odniesienia do tego węzła, jeśli nie wskazują już czegoś dalej. Ma to na celu zapewnienie, że jeśli wątek przestanie dzwonić, jego głowa nie zachowa odniesienia do węzła, który nie jest już potrzebny. compareAndSet()
Metoda będzie upewnić się, że tylko zaktualizować węzeł jeśli jakiś inny wątek nie zmienił się, ponieważ została pobrana.
Ogłoś tablicę i pomoc
Kluczem do uczynienia tego podejścia bez czekania, a nie po prostu bez blokady, jest to, że nie możemy zakładać, że harmonogram wątków da pierwszeństwo każdemu wątkowi, gdy będzie tego potrzebował. Jeśli każdy wątek po prostu spróbuje zsekwencjonować swoje własne węzły, możliwe jest, że wątek może być ciągle opróżniany pod obciążeniem. Aby uwzględnić tę możliwość, każdy wątek najpierw spróbuje „pomóc” innym wątkom, których sekwencjonowanie może nie być możliwe.
Podstawową ideą jest to, że ponieważ każdy wątek z powodzeniem tworzy węzły, przypisane sekwencje rosną monotonicznie. Jeśli wątek lub wątki ciągle przeczą innemu wątkowi, indeks używany do znajdowania niesekwencjonowanych węzłów w announce
tablicy przesunie się do przodu. Nawet jeśli każdy wątek, który obecnie próbuje sekwencjonować dany węzeł, jest stale przejmowany przez inny wątek, ostatecznie wszystkie wątki będą próbowały sekwencjonować ten węzeł. Aby to zilustrować, skonstruujemy przykład z trzema wątkami.
W punkcie początkowym wszystkie głowy i trzy elementy wątku są wskazywane w tail
węźle. lastSequence
Dla każdej nici 0.
W tym momencie Wątek 1 jest wykonywany za pomocą wywołania. Sprawdza tablicę zapowiedzi pod kątem jej ostatniej sekwencji (zero), która jest węzłem, który ma obecnie indeksować. Sekwencjonuje węzeł i lastSequence
jest ustawiony na 1.
Wątek 2 jest teraz wykonywany z wywołaniem, sprawdza tablicę zapowiedzi w ostatniej sekwencji (zero) i widzi, że nie potrzebuje pomocy, więc próbuje sekwencjonować swoje wywołanie. Udaje się, a teraz lastSequence
jest ustawiony na 2.
Wątek 3 jest teraz wykonywany, a także widzi, że węzeł w announce[0]
jest już zsekwencjonowany i sekwensuje własne wywołanie. Teraz lastSequence
jest ustawiony na 3.
Teraz wątek 1 jest wywoływany ponownie. Sprawdza tablicę ogłoszeń w indeksie 1 i stwierdza, że jest już zsekwencjonowana. Jednocześnie wywoływany jest wątek 2 . Sprawdza tablicę ogłoszeń w indeksie 2 i stwierdza, że jest już zsekwencjonowana. Zarówno Wątek 1, jak i Wątek 2 próbują teraz sekwencjonować własne węzły. Wątek 2 wygrywa i sekwencjonuje jego wywołanie. To lastSequence
jest ustawiony na 4. Tymczasem trzy wątek został wywołany. Sprawdza indeks lastSequence
(mod 3) i stwierdza, że węzeł w announce[0]
nie został zsekwencjonowany. Wątek 2 jest ponownie wywoływany w tym samym czasie, gdy wątek 1 jest podczas drugiej próby. Wątek 1znajduje nie wywoływane sekwencyjne wywołanie, w announce[1]
którym jest węzeł właśnie utworzony przez wątek 2 . Próbuje sekwencjonować wywołanie wątku 2 i kończy się powodzeniem. Wątek 2 znajduje swój własny węzeł announce[1]
i został zsekwencjonowany. Ustawia to na lastSequence
5. Wątek 3 jest następnie wywoływany i stwierdza, że węzeł, w którym umieszczony announce[0]
jest wątek 1, nadal nie jest sekwencjonowany i próbuje to zrobić. W międzyczasie wywołano także wątek 2, który uprzedza wątek 3. Sekwencjonuje swój węzeł i ustawia go na lastSequence
6.
Słaby wątek 1 . Mimo że wątek 3 próbuje go sekwencjonować, oba wątki są ciągle udaremniane przez program planujący. Ale w tym momencie. Wątek 2 wskazuje teraz również na announce[0]
(6 mod 3). Wszystkie trzy wątki są ustawione tak, aby próbować sekwencjonować to samo wywołanie. Bez względu na to, który wątek się powiedzie, następnym węzłem, który ma być zsekwencjonowany, będzie oczekujące wywołanie wątku 1, tj. Węzła, do którego się odwołuje announce[0]
.
To jest nieuniknione. Aby wątki zostały uprzednio opróżnione, inne wątki muszą być węzłami sekwencjonowania, a gdy to robią, będą stale poruszać się do lastSequence
przodu. Jeśli węzeł danego wątku nie jest ciągle sekwencjonowany, ostatecznie wszystkie wątki będą wskazywały na jego indeks w tablicy zapowiedzi. Żaden wątek nie zrobi nic innego, dopóki węzeł, któremu próbuje pomóc, nie zostanie zsekwencjonowany, najgorszym scenariuszem jest to, że wszystkie wątki wskazują ten sam niesekwencjonowany węzeł. Dlatego czas wymagany do sekwencjonowania dowolnego wywołania jest funkcją liczby wątków, a nie wielkości danych wejściowych.