Ecco una spiegazione ed un esempio di come questo è realizzato. Fammi sapere se ci sono parti che non sono chiare.
Gist con fonte
universale
Inizializzazione:
Gli indici dei thread vengono applicati in modo atomicamente incrementato. Questo è gestito usando un AtomicInteger
nome nextIndex
. Questi indici sono assegnati ai thread attraverso ThreadLocal
un'istanza che si inizializza ottenendo l'indice successivo nextIndex
e incrementandolo. Ciò accade la prima volta che viene recuperato l'indice di ogni thread la prima volta. A ThreadLocal
viene creato per tenere traccia dell'ultima sequenza creata da questo thread. È inizializzato 0. Il riferimento sequenziale dell'oggetto factory viene passato e memorizzato. AtomicReferenceArray
Vengono create due istanze di dimensioni n
. L'oggetto tail viene assegnato a ciascun riferimento, essendo stato inizializzato con lo stato iniziale fornito dalla Sequential
fabbrica. n
è il numero massimo di thread consentiti. Ogni elemento in queste matrici 'appartiene' all'indice del thread corrispondente.
Applicare metodo:
Questo è il metodo che fa il lavoro interessante. Fa quanto segue:
- Crea un nuovo nodo per questa chiamata: il mio
- Imposta questo nuovo nodo nella matrice di annuncio nell'indice del thread corrente
Quindi inizia il loop di sequenziamento. Continuerà fino a quando l'attuale chiamata non sarà stata sequenziata:
- trova un nodo nella matrice di annuncio usando la sequenza dell'ultimo nodo creato da questo thread. Più su questo più tardi.
- se viene trovato un nodo nel passaggio 2 non è ancora in sequenza, continuare con esso, altrimenti concentrarsi solo sull'invocazione corrente. Questo tenterà di aiutare solo un altro nodo per invocazione.
- Qualunque nodo sia stato selezionato nel passaggio 3, continua a provare a sequenziarlo dopo l'ultimo nodo sequenziato (altri thread potrebbero interferire.) Indipendentemente dal successo, imposta il riferimento head dei thread correnti sulla sequenza restituita da
decideNext()
La chiave per il ciclo nidificato sopra descritto è il decideNext()
metodo. Per capirlo, dobbiamo guardare alla classe Node.
Classe di nodo
Questa classe specifica i nodi in un elenco doppiamente collegato. Non c'è molta azione in questa classe. La maggior parte dei metodi sono semplici metodi di recupero che dovrebbero essere abbastanza autoesplicativi.
metodo di coda
questo restituisce un'istanza di nodo speciale con una sequenza di 0. Funziona semplicemente come segnaposto fino a quando non viene sostituita da una chiamata.
Proprietà e inizializzazione
seq
: il numero progressivo, inizializzato su -1 (che significa non seguito)
invocation
: il valore dell'invocazione di apply()
. Impostato su costruzione.
next
: AtomicReference
per il collegamento in avanti. una volta assegnato, questo non verrà mai modificato
previous
: AtomicReference
per il collegamento a ritroso assegnato al sequenziamento e cancellato datruncate()
Decidi il prossimo
Questo metodo è solo uno in Nodo con logica non banale. In poche parole, un nodo viene offerto come candidato per essere il nodo successivo nell'elenco collegato. Il compareAndSet()
metodo verificherà se il riferimento è nullo e, in tal caso, imposta il riferimento sul candidato. Se il riferimento è già impostato, non fa nulla. Questa operazione è atomica, quindi se due candidati vengono offerti nello stesso momento, ne verrà selezionato solo uno. Questo garantisce che un solo nodo sarà mai selezionato come il prossimo. Se viene selezionato il nodo candidato, la sequenza viene impostata sul valore successivo e il collegamento precedente viene impostato su questo nodo.
Tornare alla classe universale applica metodo ...
Avendo richiamato decideNext()
l'ultimo nodo in sequenza (se selezionato) con il nostro nodo o un nodo announce
dall'array, ci sono due possibili occorrenze: 1. Il nodo è stato sequenziato con successo 2. Qualche altro thread ha preceduto questo thread.
Il prossimo passo è verificare se il nodo creato per questa chiamata. Questo potrebbe accadere perché questo thread lo ha sequenziato correttamente o qualche altro thread lo ha raccolto announce
dall'array e lo ha sequenziato per noi. Se non è stato sequenziato, il processo viene ripetuto. In caso contrario, la chiamata termina cancellando l'array di annuncio nell'indice di questo thread e restituendo il valore del risultato dell'invocazione. L'array di annuncio viene cancellato per garantire che non vi siano riferimenti al nodo lasciato in giro che impedirebbero la raccolta di dati inutili del nodo e, pertanto, manterrebbero vivi sull'heap tutti i nodi dell'elenco collegato da quel punto in poi.
Valuta il metodo
Ora che il nodo dell'invocazione è stato sequenziato correttamente, l'invocazione deve essere valutata. Per fare ciò, il primo passo è garantire che le invocazioni precedenti a questa siano state valutate. Se non hanno questo thread non aspetteranno ma funzioneranno immediatamente.
Metodo SecurePrior
Il ensurePrior()
metodo funziona in questo modo controllando il nodo precedente nell'elenco collegato. Se il suo stato non è impostato, verrà valutato il nodo precedente. Nodo che questo è ricorsivo. Se il nodo precedente al nodo precedente non è stato valutato, chiamerà valutare per quel nodo e così via.
Ora che è noto che il nodo precedente ha uno stato, possiamo valutare questo nodo. L'ultimo nodo viene recuperato e assegnato a una variabile locale. Se questo riferimento è nullo, significa che qualche altro thread ha preceduto questo thread e ha già valutato questo nodo; impostando lo stato. Altrimenti, lo stato del nodo precedente viene passato al Sequential
metodo di applicazione dell'oggetto insieme all'invocazione di questo nodo. Lo stato restituito viene impostato sul nodo e truncate()
viene chiamato il metodo, cancellando il collegamento all'indietro dal nodo in quanto non è più necessario.
Metodo MoveForward
Il metodo di spostamento in avanti tenterà di spostare tutti i riferimenti principali a questo nodo se non stanno già indicando qualcosa di più lungo. Questo per garantire che se un thread smette di chiamare, il suo head non manterrà un riferimento a un nodo che non è più necessario. Il compareAndSet()
metodo assicurerà di aggiornare il nodo solo se qualche altro thread non lo ha modificato da quando è stato recuperato.
Annunciare array e aiutare
La chiave per rendere questo approccio senza attesa invece che semplicemente senza lock è che non possiamo presumere che lo scheduler dei thread darà a ogni thread la priorità quando ne ha bisogno. Se ogni thread ha semplicemente tentato di mettere in sequenza i propri nodi, è possibile che un thread possa essere continuamente prevenuto sotto carico. Per tenere conto di questa possibilità, ogni thread tenterà innanzitutto di "aiutare" altri thread che potrebbero non essere in grado di essere sequenziati.
L'idea di base è che mentre ogni thread crea correttamente nodi, le sequenze assegnate stanno aumentando monotonicamente. Se un thread o thread continuano a precludere un altro thread, l'indice utilizzato per trovare nodi non seguiti nella announce
matrice si sposterà in avanti. Anche se ogni thread che sta attualmente tentando di sequenziare un determinato nodo viene continuamente prevenuto da un altro thread, alla fine tutti i thread cercheranno di sequenziare quel nodo. Per illustrare, costruiremo un esempio con tre thread.
Al punto di partenza, la testa di tutti e tre i thread e gli elementi di annuncio sono puntati sul tail
nodo. Il lastSequence
per ogni thread è 0.
A questo punto, il thread 1 viene eseguito con una chiamata. Controlla la matrice di annuncio per l'ultima sequenza (zero) che è il nodo che è attualmente pianificato per indicizzare. Segue il nodo ed lastSequence
è impostato su 1.
Il thread 2 viene ora eseguito con un richiamo, controlla l'array di annuncio nell'ultima sequenza (zero) e vede che non ha bisogno di aiuto e quindi tenta di sequenziare il suo richiamo. Ci riesce e ora lastSequence
è impostato su 2.
Il thread 3 è ora eseguito e vede anche che il nodo in announce[0]
è già in sequenza e sequenze è la propria chiamata. Ora lastSequence
è impostato su 3.
Ora il thread 1 viene nuovamente richiamato. Controlla la matrice di annuncio all'indice 1 e trova che è già in sequenza. Allo stesso tempo, viene invocato il thread 2 . Controlla la matrice di annuncio all'indice 2 e trova che è già in sequenza. Sia Thread 1 che Thread 2 ora tentano di sequenziare i propri nodi. Il thread 2 vince e sequenzia la sua invocazione. È lastSequence
impostato su 4. Nel frattempo, è stato richiamato il thread tre. Controlla l'indice lastSequence
(mod 3) e trova che il nodo in announce[0]
non è stato sequenziato. Il thread 2 viene nuovamente invocato nello stesso momento in cui il thread 1 è al secondo tentativo. Discussione 1trova un'invocazione senza conseguenze in announce[1]
cui si trova il nodo appena creato da Thread 2 . Tenta di mettere in sequenza l' invocazione di Thread 2 e ha esito positivo. Il thread 2 trova il proprio nodo su announce[1]
ed è stato sequenziato. È impostato lastSequence
su 5. Il thread 3 viene quindi richiamato e trova quel nodo in cui si trova il thread 1 announce[0]
non è ancora in sequenza e tenta di farlo. Nel frattempo anche il thread 2 è stato invocato e impedisce il thread 3. Mette in sequenza il suo nodo e lo imposta lastSequence
su 6.
Discussione scadente 1 . Anche se Thread 3 sta cercando di sequenziarlo, entrambi i thread sono stati continuamente contrastati dallo scheduler. Ma a questo punto. Il thread 2 ora punta anche a announce[0]
(6 mod 3). Tutti e tre i thread sono impostati per tentare di sequenziare la stessa chiamata. Indipendentemente dal thread con esito positivo, il nodo successivo da sequenziare sarà l'invocazione in attesa del thread 1, ovvero il nodo a cui fa riferimento announce[0]
.
Questo è inevitabile Affinché i thread possano essere prevenuti, altri thread devono sequenziare i nodi e, mentre lo fanno, si sposteranno continuamente in lastSequence
avanti. Se il nodo di un determinato thread non viene continuamente sequenziato, alla fine tutti i thread indicheranno il suo indice nella matrice di annuncio. Nessun thread farà altro fino a quando il nodo che sta cercando di aiutare non è stato sequenziato, lo scenario peggiore è che tutti i thread puntano allo stesso nodo non seguito. Pertanto, il tempo necessario per sequenziare qualsiasi invocazione dipende dal numero di thread e non dalla dimensione dell'input.