Una "construcción universal" es una clase de contenedor para un objeto secuencial que permite que se linealice (una condición de consistencia fuerte para objetos concurrentes). Por ejemplo, aquí hay una construcción adaptada sin esperas, en Java, de [1], que presume la existencia de una cola sin esperas que satisface la interfaz WFQ
(que solo requiere un consenso entre hilos) y asume una Sequential
interfaz:
public interface WFQ<T> // "FIFO" iteration
{
int enqueue(T t); // returns the sequence number of t
Iterable<T> iterateUntil(int max); // iterates until sequence max
}
public interface Sequential
{
// Apply an invocation (method + arguments)
// and get a response (return value + state)
Response apply(Invocation i);
}
public interface Factory<T> { T generate(); } // generate new default object
public interface Universal extends Sequential {}
public class SlowUniversal implements Universal
{
Factory<? extends Sequential> generator;
WFQ<Invocation> wfq = new WFQ<Invocation>();
Universal(Factory<? extends Sequential> g) { generator = g; }
public Response apply(Invocation i)
{
int max = wfq.enqueue(i);
Sequential s = generator.generate();
for(Invocation invoc : wfq.iterateUntil(max))
s.apply(invoc);
return s.apply(i);
}
}
Esta implementación no es muy satisfactoria, ya que es realmente lenta (recuerda cada invocación y tiene que reproducirla en cada solicitud; tenemos un tiempo de ejecución lineal en el tamaño del historial). ¿Hay alguna forma de ampliar las interfaces WFQ
y Sequential
(de manera razonable) para permitirnos guardar algunos pasos al aplicar una nueva invocación?
¿Podemos hacer esto más eficiente (no el tiempo de ejecución lineal en el tamaño del historial, preferiblemente el uso de memoria también disminuye) sin perder la propiedad sin esperas?
Aclaración
Una "construcción universal" es un término que estoy seguro de que fue creado por [1] que acepta un objeto inseguro pero compatible con hilos, que es generalizado por la Sequential
interfaz. Usando una cola sin esperas, la primera construcción ofrece una versión linealizable del objeto que también es libre de esperas (esto supone apply
operaciones de determinismo y detención ).
Esto es ineficiente, ya que el método consiste en hacer que cada subproceso local comience desde una pizarra limpia y aplique todas las operaciones que se hayan registrado. En cualquier caso, esto funciona porque logra una sincronización efectiva mediante el uso de WFQ
para determinar el orden en el que se deben aplicar todas las operaciones: cada llamada de subproceso apply
verá el mismo Sequential
objeto local , con la misma secuencia de Invocation
s aplicada.
Mi pregunta es si podemos (por ejemplo) introducir un proceso de limpieza en segundo plano que actualice el "estado inicial" para que no tengamos que reiniciar desde cero. Esto no es tan simple como tener un puntero atómico con un puntero de inicio: este tipo de enfoques pierden fácilmente la garantía sin esperas. Mi sospecha es que algún otro enfoque basado en colas podría funcionar aquí.
Jerga:
- sin esperas: independientemente de la cantidad de subprocesos o de la toma de decisiones del planificador,
apply
terminará en un número de instrucciones probadamente acotadas ejecutadas para ese subproceso. - sin bloqueo: igual que el anterior, pero admite la posibilidad de un tiempo de ejecución ilimitado, solo en el caso de que
apply
se realice un número ilimitado de operaciones en otros subprocesos. Por lo general, los esquemas de sincronización optimistas entran en esta categoría. - bloqueo - eficiencia a merced del planificador.
Un ejemplo de trabajo, según lo solicitado (ahora en una página que no caducará)
[1] Herlihy y Shavit, El arte de la programación multiprocesador .
CopyableSequential
sean válidas; la linealización debería seguir al hecho de que es asíSequential
.Respuestas:
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
nombrenextIndex
. Estos índices se asignan a subprocesos a través de unaThreadLocal
instancia que se inicializa obteniendo el siguiente índicenextIndex
e incrementándolo. Esto sucede la primera vez que se recupera el índice de cada subproceso la primera vez. AThreadLocal
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. SeAtomicReferenceArray
crean dos instancias de tamañon
. El objeto de cola se asigna a cada referencia, habiéndose inicializado con el estado inicial proporcionado por laSequential
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:
Entonces comienza el ciclo de secuenciación. Continuará hasta que la invocación actual se haya secuenciado:
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 deapply()
. En construcción.next
:AtomicReference
para el enlace directo. una vez asignado, esto nunca será cambiadoprevious
: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 laannounce
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 ytruncate()
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. EllastSequence
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. AhoralastSequence
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 índicelastSequence
(mod 3) y descubre que el nodo enannounce[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 laannounce[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 nodoannounce[1]
y ha sido secuenciado. Establece que eslastSequence
5. El subproceso 3 se invoca y encuentra que el nodo en el que se colocó el subproceso 1announce[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 establecelastSequence
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 referenciaannounce[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.fuente
previous
ynext
para ser válido. Mantener y crear un historial válido sin esperas parece difícil.Mi respuesta anterior realmente no responde la pregunta correctamente, pero como el OP lo ve como útil, lo dejaré como está. Según el código en el enlace de la pregunta, aquí está mi intento. Solo he realizado pruebas realmente básicas sobre esto, pero parece que calcula los promedios correctamente. Se agradecen los comentarios sobre si esto es adecuadamente sin esperas.
NOTA : Eliminé la interfaz Universal y la convertí en una clase. Tener Universal compuesto de secuencias y ser uno parece una complicación innecesaria, pero podría estar perdiendo algo. En la clase promedio, he marcado la variable de estado para ser
volatile
. Esto no es necesario para que el código funcione. Para ser conservador (una buena idea con subprocesos) y evitar que cada subproceso haga todos los cálculos (una vez).Secuencial y Fábrica
Universal
Promedio
Código de demostración
Hice algunas ediciones al código mientras lo publicaba aquí. Debería estar bien, pero avíseme si tiene problemas con él.
fuente
wfq
, por lo que aún tiene que recorrer toda la historia: el tiempo de ejecución no ha mejorado, excepto por un factor constante.