Aquí hay una explicación y un ejemplo de cómo se logra esto. Avíseme si hay partes que no están claras.
Gist con fuente
Universal
Inicializacion:
Los índices de subprocesos se aplican de manera atómicamente incrementada. Esto se gestiona utilizando un AtomicInteger
nombre nextIndex
. Estos índices se asignan a subprocesos a través de una ThreadLocal
instancia que se inicializa obteniendo el siguiente índice nextIndex
e incrementándolo. Esto sucede la primera vez que se recupera el índice de cada subproceso la primera vez. A ThreadLocal
se crea para rastrear la última secuencia que creó este hilo. Se inicializa 0. La referencia de objeto de fábrica secuencial se pasa y se almacena. Se AtomicReferenceArray
crean dos instancias de tamaño n
. El objeto de cola se asigna a cada referencia, habiéndose inicializado con el estado inicial proporcionado por la Sequential
fábrica. n
es el número máximo de hilos permitido. Cada elemento en estas matrices 'pertenece' al índice de subprocesos correspondiente.
Aplicar método:
Este es el método que hace el trabajo interesante. Hace lo siguiente:
- Cree un nuevo nodo para esta invocación: mine
- Establezca este nuevo nodo en la matriz de anuncios en el índice del hilo actual
Entonces comienza el ciclo de secuenciación. Continuará hasta que la invocación actual se haya secuenciado:
- encuentre un nodo en la matriz de anuncios utilizando la secuencia del último nodo creado por este hilo. Más sobre esto más tarde.
- si se encuentra un nodo en el paso 2, aún no está secuenciado, continúe con él, de lo contrario, solo concéntrese en la invocación actual. Esto solo intentará ayudar a otro nodo por invocación.
- Cualquiera que sea el nodo seleccionado en el paso 3, siga intentando secuenciarlo después del último nodo secuenciado (pueden interferir otros subprocesos). Independientemente del éxito, establezca la referencia del encabezado de subprocesos actual a la secuencia devuelta por
decideNext()
La clave del bucle anidado descrito anteriormente es el decideNext()
método. Para entender eso, necesitamos mirar la clase Node.
Clase de nodo
Esta clase especifica nodos en una lista doblemente vinculada. No hay mucha acción en esta clase. La mayoría de los métodos son métodos de recuperación simples que deberían explicarse por sí mismos.
método de cola
esto devuelve una instancia de nodo especial con una secuencia de 0. Simplemente actúa como un marcador de posición hasta que una invocación lo reemplaza.
Propiedades e inicialización
seq
: el número de secuencia, inicializado a -1 (es decir, sin secuencia)
invocation
: el valor de la invocación de apply()
. En construcción.
next
: AtomicReference
para el enlace directo. una vez asignado, esto nunca será cambiado
previous
: AtomicReference
para el enlace hacia atrás asignado al secuenciar y borrado portruncate()
Decidir a continuación
Este método es solo uno en Nodo con lógica no trivial. En pocas palabras, se ofrece un nodo como candidato para ser el siguiente nodo en la lista vinculada. El compareAndSet()
método verificará si su referencia es nula y, de ser así, establecerá la referencia al candidato. Si la referencia ya está establecida, no hace nada. Esta operación es atómica, por lo que si se ofrecen dos candidatos en el mismo momento, solo se seleccionará uno. Esto garantiza que solo se seleccionará un nodo como el siguiente. Si se selecciona el nodo candidato, su secuencia se establece en el siguiente valor, y su enlace anterior se establece en este nodo.
Volver al método de aplicación de la clase Universal ...
Habiendo llamado decideNext()
al último nodo secuenciado (cuando está marcado) con nuestro nodo o un nodo de la announce
matriz, hay dos posibles ocurrencias: 1. El nodo fue secuenciado exitosamente 2. Algún otro hilo se adelantó a este hilo.
El siguiente paso es verificar si el nodo creado para esta invocación. Esto podría suceder porque este hilo lo ha secuenciado con éxito o algún otro hilo lo recogió de la announce
matriz y lo ha secuenciado para nosotros. Si no se ha secuenciado, el proceso se repite. De lo contrario, la llamada finaliza al borrar la matriz de anuncio en el índice de este hilo y devolver el valor del resultado de la invocación. La matriz de anuncio se borra para garantizar que no queden referencias al nodo que evite que el nodo se recolecte basura y, por lo tanto, mantenga todos los nodos en la lista vinculada desde ese punto en vivo en el montón.
Evaluar método
Ahora que el nodo de la invocación se ha secuenciado correctamente, la invocación debe evaluarse. Para hacer eso, el primer paso es asegurar que las invocaciones anteriores a esta hayan sido evaluadas. Si no tienen este hilo, no esperará pero hará ese trabajo de inmediato.
Garantizar método anterior
El ensurePrior()
método hace esto comprobando el nodo anterior en la lista vinculada. Si su estado no está establecido, se evaluará el nodo anterior. Nodo de que esto es recursivo. Si el nodo anterior al nodo anterior no ha sido evaluado, llamará a la evaluación para ese nodo y así sucesivamente.
Ahora que se sabe que el nodo anterior tiene un estado, podemos evaluar este nodo. El último nodo se recupera y se asigna a una variable local. Si esta referencia es nula, significa que algún otro subproceso se ha adelantado a este y ya ha evaluado este nodo; estableciendo su estado. De lo contrario, el estado del nodo anterior se pasa al Sequential
método de aplicación del objeto junto con la invocación de este nodo. El estado devuelto se establece en el nodo y truncate()
se llama al método, borrando el enlace hacia atrás del nodo ya que ya no es necesario.
Método MoveForward
El método de avance intentará mover todas las referencias de cabeza a este nodo si aún no apuntan a algo más adelante. Esto es para garantizar que si un subproceso deja de llamar, su cabecera no retendrá una referencia a un nodo que ya no sea necesario. El compareAndSet()
método se asegurará de que solo actualicemos el nodo si algún otro hilo no lo ha cambiado desde que se recuperó.
Anunciar matriz y ayudar
La clave para hacer que este enfoque sea libre de espera en lugar de simplemente libre de bloqueo es que no podemos suponer que el planificador de subprocesos dará prioridad a cada subproceso cuando lo necesite. Si cada subproceso simplemente intentó secuenciar sus propios nodos, es posible que un subproceso se pueda adelantar continuamente bajo carga. Para tener en cuenta esta posibilidad, cada subproceso primero intentará 'ayudar' a otros subprocesos que no puedan secuenciarse.
La idea básica es que a medida que cada subproceso crea con éxito nodos, las secuencias asignadas aumentan monotónicamente. Si un subproceso o subprocesos se adelantan continuamente a otro subproceso, el índice del uso para encontrar nodos no secuenciados en la announce
matriz avanzará. Incluso si cada subproceso que actualmente está tratando de secuenciar un nodo dado se ve continuamente reemplazado por otro subproceso, eventualmente todos los subprocesos intentarán secuenciar ese nodo. Para ilustrar, construiremos un ejemplo con tres hilos.
En el punto de partida, los elementos de cabeza y anuncio de los tres hilos apuntan al tail
nodo. El lastSequence
para cada hilo es 0.
En este punto, el hilo 1 se ejecuta con una invocación. Comprueba la matriz de anuncio para su última secuencia (cero), que es el nodo que está programado para indexar actualmente. Secuencia el nodo y se lastSequence
establece en 1.
El subproceso 2 ahora se ejecuta con una invocación, comprueba la matriz de anuncios en su última secuencia (cero) y ve que no necesita ayuda, por lo que intenta secuenciar su invocación. Tiene éxito y ahora lastSequence
está configurado en 2.
El subproceso 3 ahora se ejecuta y también ve que el nodo en announce[0]
ya está secuenciado y secuencia su propia invocación. Ahora lastSequence
está configurado en 3.
Ahora se vuelve a invocar el hilo 1 . Comprueba la matriz de anuncios en el índice 1 y descubre que ya está secuenciada. Al mismo tiempo, se invoca el hilo 2 . Comprueba la matriz de anuncios en el índice 2 y descubre que ya está secuenciada. Tanto el subproceso 1 como el subproceso 2 ahora intentan secuenciar sus propios nodos. El hilo 2 gana y secuencia su invocación. Está lastSequence
configurado en 4. Mientras tanto, se ha invocado el hilo tres. Comprueba el índice lastSequence
(mod 3) y descubre que el nodo en announce[0]
no ha sido secuenciado. El hilo 2 se invoca nuevamente al mismo tiempo que el hilo 1 está en su segundo intento. Hilo 1encuentra una invocación no secuenciada en la announce[1]
que se encuentra el nodo recién creado por Thread 2 . Intenta secuenciar la invocación de Thread 2 y tiene éxito. El hilo 2 encuentra su propio nodo announce[1]
y ha sido secuenciado. Establece que es lastSequence
5. El subproceso 3 se invoca y encuentra que el nodo en el que se colocó el subproceso 1 announce[0]
todavía no está secuenciado e intenta hacerlo. Mientras tanto, el hilo 2 también se ha invocado y se adelanta al hilo 3. Secuencia su nodo y lo establece lastSequence
en 6.
Hilo pobre 1 . A pesar de que Thread 3 está tratando de secuenciarlo, ambos hilos han sido continuamente frustrados por el programador. Pero en este punto. El hilo 2 ahora también apunta a announce[0]
(6 mod 3). Los tres hilos están configurados para intentar secuenciar la misma invocación. No importa qué subproceso tenga éxito, el siguiente nodo que se secuenciará será la invocación en espera del Subproceso 1, es decir, el nodo al que hace referencia announce[0]
.
Esto es inevitable Para que los subprocesos se vacíen, otros subprocesos deben ser nodos de secuencia y, a medida que lo hacen, continuamente avanzarán lastSequence
. Si el nodo de un subproceso dado no se secuencia continuamente, eventualmente todos los subprocesos apuntarán a su índice en la matriz de anuncios. Ningún subproceso hará otra cosa hasta que el nodo al que intenta ayudar se haya secuenciado, el peor de los casos es que todos los subprocesos estén apuntando al mismo nodo no secuenciado. Por lo tanto, el tiempo requerido para secuenciar cualquier invocación es una función del número de subprocesos y no del tamaño de la entrada.