Voici une explication et un exemple de la façon dont cela est accompli. Faites-moi savoir s'il y a des parties qui ne sont pas claires.
Gist avec source
Universel
Initialisation:
Les index de threads sont appliqués de manière atomiquement incrémentée. Ceci est géré à l'aide d'un AtomicInteger
nommé nextIndex
. Ces index sont affectés aux threads via une ThreadLocal
instance qui s'initialise en récupérant l'index suivant nextIndex
et en l'incrémentant. Cela se produit la première fois que l'index de chaque thread est récupéré la première fois. Un ThreadLocal
est créé pour suivre la dernière séquence créée par ce thread. Il est initialisé à 0. La référence d'objet de fabrique séquentielle est transmise et stockée. Deux AtomicReferenceArray
instances sont créées de taille n
. L'objet de queue est affecté à chaque référence, après avoir été initialisé avec l'état initial fourni par l' Sequential
usine. n
est le nombre maximal de threads autorisés. Chaque élément de ces tableaux «appartient» à l'index de thread correspondant.
Appliquer la méthode:
C'est la méthode qui fait le travail intéressant. Il fait ce qui suit:
- Créez un nouveau nœud pour cette invocation: le mien
- Définissez ce nouveau nœud dans le tableau d'annonce à l'index du thread actuel
Ensuite, la boucle de séquençage commence. Elle se poursuivra jusqu'à ce que l'invocation en cours soit séquencée:
- rechercher un nœud dans le tableau d'annonce en utilisant la séquence du dernier nœud créé par ce thread. Plus d'informations à ce sujet plus tard.
- si un nœud est trouvé à l'étape 2, il n'est pas encore séquencé, continuez-le, sinon, concentrez-vous simplement sur l'invocation en cours. Cela essaiera seulement d'aider un autre nœud par appel.
- Quel que soit le nœud sélectionné à l'étape 3, continuez d'essayer de le séquencer après le dernier nœud séquencé (d'autres threads peuvent interférer.) Quel que soit le succès, définissez la référence de tête de threads actuelle sur la séquence renvoyée par
decideNext()
La clé de la boucle imbriquée décrite ci-dessus est la decideNext()
méthode. Pour comprendre cela, nous devons regarder la classe Node.
Classe de noeud
Cette classe spécifie les nœuds dans une liste à double liaison. Il n'y a pas beaucoup d'action dans cette classe. La plupart des méthodes sont de simples méthodes de récupération qui devraient être assez explicites.
méthode de la queue
cela renvoie une instance de nœud spéciale avec une séquence de 0. Elle agit simplement comme un espace réservé jusqu'à ce qu'une invocation la remplace.
Propriétés et initialisation
seq
: le numéro de séquence, initialisé à -1 (signifiant non séquencé)
invocation
: la valeur de l'invocation de apply()
. Mis en construction.
next
: AtomicReference
pour la liaison directe. une fois attribué, cela ne sera jamais changé
previous
: AtomicReference
pour le lien arrière attribué lors du séquençage et effacé partruncate()
Décider ensuite
Cette méthode n'est qu'une dans Node avec une logique non triviale. En résumé, un nœud est proposé comme candidat pour être le nœud suivant dans la liste chaînée. La compareAndSet()
méthode vérifiera si sa référence est nulle et si oui, définissez la référence au candidat. Si la référence est déjà définie, elle ne fait rien. Cette opération est atomique donc si deux candidats sont proposés en même temps, un seul sera sélectionné. Cela garantit qu'un seul nœud sera jamais sélectionné comme prochain. Si le nœud candidat est sélectionné, sa séquence est définie sur la valeur suivante et son lien précédent est défini sur ce nœud.
Retour à la méthode d'application de la classe universelle ...
Après avoir appelé decideNext()
le dernier nœud séquencé (lorsqu'il est vérifié) avec notre nœud ou un nœud de la announce
baie, il y a deux occurrences possibles: 1. Le nœud a été séquencé avec succès 2. Un autre thread a anticipé ce thread.
L'étape suivante consiste à vérifier si le nœud créé pour cette invocation. Cela peut se produire parce que ce thread l'a séquencé avec succès ou qu'un autre thread l'a récupéré du announce
tableau et l'a séquencé pour nous. S'il n'a pas été séquencé, le processus est répété. Sinon, l'appel se termine en effaçant le tableau d'annonces de l'index de ce thread et en renvoyant la valeur de résultat de l'invocation. Le tableau d'annonce est effacé pour garantir qu'il n'y a aucune référence au nœud laissé autour qui empêcherait le nœud d'être récupéré et donc garder tous les nœuds de la liste liée à partir de ce point en vie sur le tas.
Évaluer la méthode
Maintenant que le nœud de l'invocation a été correctement séquencé, l'invocation doit être évaluée. Pour ce faire, la première étape consiste à s'assurer que les invocations précédant celle-ci ont été évaluées. S'ils n'ont pas ce fil, ils n'attendront pas, mais ils le feront immédiatement.
Méthode EnsurePrior
La ensurePrior()
méthode effectue ce travail en vérifiant le nœud précédent dans la liste chaînée. Si son état n'est pas défini, le nœud précédent sera évalué. Nœud que c'est récursif. Si le nœud avant le nœud précédent n'a pas été évalué, il appellera évaluer pour ce nœud et ainsi de suite.
Maintenant que le nœud précédent est connu pour avoir un état, nous pouvons évaluer ce nœud. Le dernier nœud est récupéré et affecté à une variable locale. Si cette référence est nulle, cela signifie qu'un autre thread a anticipé celui-ci et a déjà évalué ce nœud; mettre son état. Sinon, l'état du nœud précédent est transmis à la Sequential
méthode apply de l' objet avec l'invocation de ce nœud. L'état renvoyé est défini sur le nœud et la truncate()
méthode est appelée, supprimant le lien arrière du nœud car il n'est plus nécessaire.
Méthode MoveForward
La méthode Move Forward tentera de déplacer toutes les références de tête vers ce nœud si elles ne pointent pas déjà vers quelque chose plus loin. Cela permet de garantir que si un thread cesse d'appeler, sa tête ne conservera pas de référence à un nœud qui n'est plus nécessaire. La compareAndSet()
méthode s'assurera que nous ne mettons à jour le nœud que si un autre thread ne l'a pas modifié depuis qu'il a été récupéré.
Annoncer la baie et aider
La clé pour rendre cette approche sans attente par opposition à simplement sans verrouillage est que nous ne pouvons pas supposer que le planificateur de threads donnera la priorité à chaque thread quand il en aura besoin. Si chaque thread tente simplement de séquencer ses propres nœuds, il est possible qu'un thread soit continuellement préempté sous charge. Pour tenir compte de cette possibilité, chaque thread essaiera d'abord d'aider les autres threads qui ne pourront peut-être pas être séquencés.
L'idée de base est que chaque thread créant avec succès des nœuds, les séquences affectées augmentent de façon monotone. Si un thread ou des threads préemptent continuellement un autre thread, l'index utilisé pour rechercher les nœuds non séquencés dans le announce
tableau avancera. Même si chaque thread qui essaie actuellement de séquencer un nœud donné est continuellement préempté par un autre thread, tous les threads tenteront éventuellement de séquencer ce nœud. Pour illustrer, nous allons construire un exemple avec trois threads.
Au point de départ, la tête et les éléments d'annonce des trois threads sont pointés vers le tail
nœud. Le lastSequence
pour chaque thread est 0.
À ce stade, le thread 1 est exécuté avec une invocation. Il vérifie le tableau d'annonce pour sa dernière séquence (zéro) qui est le nœud qu'il est actuellement programmé pour indexer. Il séquence le nœud et il lastSequence
est défini sur 1.
Le thread 2 est maintenant exécuté avec une invocation, il vérifie le tableau d'annonces à sa dernière séquence (zéro) et voit qu'il n'a pas besoin d'aide et tente donc de séquencer son invocation. Il réussit et maintenant il lastSequence
est réglé sur 2.
Le thread 3 est maintenant exécuté et il voit également que le nœud à announce[0]
est déjà séquencé et séquence sa propre invocation. Il lastSequence
est désormais réglé sur 3.
Le thread 1 est à nouveau invoqué. Il vérifie le tableau d'annonces à l'index 1 et constate qu'il est déjà séquencé. Simultanément, le thread 2 est appelé. Il vérifie le tableau d'annonces à l'index 2 et constate qu'il est déjà séquencé. Les deux Discussion 1 et Discussion 2 tentent maintenant de séquencer leurs propres noeuds. Le thread 2 gagne et il séquence son invocation. Il lastSequence
est défini sur 4. Pendant ce temps, le thread trois a été appelé. Il vérifie l'index lastSequence
(mod 3) et constate que le nœud à announce[0]
n'a pas été séquencé. Le thread 2 est à nouveau invoqué en même temps que le thread 1 en est à sa deuxième tentative. Fil 1trouve une invocation non séquencée à announce[1]
laquelle se trouve le nœud qui vient d'être créé par le thread 2 . Il tente de séquencer l' appel de Thread 2 et réussit. Le thread 2 trouve son propre nœud announce[1]
et il a été séquencé. Il est réglé lastSequence
sur 5. Le thread 3 est ensuite appelé et trouve que le nœud sur lequel le thread 1 est placé announce[0]
n'est toujours pas séquencé et tente de le faire. Pendant ce temps, Thread 2 a également été invoqué et préempte Thread 3. Il séquence son nœud et le définit lastSequence
sur 6.
Mauvais fil 1 . Même si Thread 3 essaie de le séquencer, les deux threads ont été continuellement contrecarrés par le planificateur. Mais à ce stade. Le thread 2 pointe également vers announce[0]
(6 mod 3). Les trois threads sont définis pour tenter de séquencer le même appel. Quel que soit le thread qui réussit, le prochain nœud à séquencer sera l'invocation en attente du thread 1, c'est-à-dire le nœud référencé par announce[0]
.
C'est inévitable. Pour que les threads soient anticipés, les autres threads doivent être des nœuds de séquençage et, ce faisant, ils avanceront continuellement lastSequence
. Si le nœud d'un thread donné n'est pas continuellement séquencé, tous les threads pointeront finalement vers son index dans le tableau d'annonce. Aucun thread ne fera quoi que ce soit tant que le noeud qu'il essaie d'aider n'a pas été séquencé, le pire des cas est que tous les threads pointent vers le même noeud non séquencé. Par conséquent, le temps requis pour séquencer toute invocation est fonction du nombre de threads et non de la taille de l'entrée.