La consistencia de versión es uno de los modelos de consistencia basados en sincronización utilizados en la programación concurrente (por ejemplo, en memoria compartida distribuida , transacciones distribuidas , etc.).
En los sistemas de computación paralela modernos , se debe mantener la consistencia de la memoria para evitar resultados indeseables. Los modelos de consistencia estricta, como la consistencia secuencial, se componen intuitivamente, pero pueden ser bastante restrictivos en términos de rendimiento, ya que deshabilitarían el paralelismo a nivel de instrucción que se aplica ampliamente en la programación secuencial. Para lograr un mejor rendimiento, se exploran algunos modelos relajados y la consistencia de liberación es un intento agresivo de relajación. [1]
La consistencia secuencial se puede lograr simplemente mediante la implementación de hardware, mientras que la consistencia de la versión también se basa en la observación de que la mayoría de los programas paralelos están sincronizados correctamente. En el nivel de programación, la sincronización se aplica para programar claramente un determinado acceso a la memoria en un hilo para que se produzca después de otro. Cuando se accede a una variable sincronizada, el hardware se aseguraría de que todas las escrituras locales de un procesador se hayan propagado a todos los demás procesadores y de que se vean y recopilen todas las escrituras de otros procesadores. En el modelo de consistencia de la versión, la acción de entrar y salir de una sección crítica se clasifica como adquisición y liberación y, para cualquier caso, se debe incluir un código explícito en el programa que muestre cuándo realizar estas operaciones .
En general, una memoria compartida distribuida es consistente con la versión si obedece las siguientes reglas: [2]
1. Antes de realizar un acceso a una variable compartida, todas las adquisiciones anteriores realizadas por este procesador deben haberse completado.
2. Antes de realizar una liberación, todas las lecturas y escrituras anteriores de este proceso deben haberse completado.
3. Los accesos de adquisición y liberación deben ser consistentes con el procesador .
Si se cumplen las condiciones anteriores y el programa está sincronizado correctamente (es decir, los procesadores implementan la adquisición y la liberación correctamente), los resultados de cualquier ejecución serán exactamente los mismos que si se hubieran ejecutado siguiendo la coherencia secuencial. En efecto, los accesos a las variables compartidas se separan en bloques de operaciones atómicas mediante las primitivas de adquisición y liberación, de modo que se evitarán las carreras y el entrelazado entre bloques.
Una liberación de bloqueo puede considerarse como un tipo de sincronización de liberación. Supongamos que se realiza una operación de bucle utilizando el código que se muestra a la derecha. Dos subprocesos intentan ingresar a una sección crítica y leer el valor más reciente de a , luego salir de la sección crítica. El código muestra que el subproceso 0 primero adquiere el bloqueo y entra en la sección crítica. Para ejecutarse correctamente, P1 debe leer el último valor de a escrito por P0. En ese caso, solo un subproceso puede estar en la sección crítica a la vez. Por lo tanto, la sincronización en sí ha asegurado que la adquisición de bloqueo exitosa en P1 ocurra después de la liberación de bloqueo por P0. Además, se debe garantizar el orden S2 -> S3, ya que P0 debe propagar el nuevo valor de a a P1. Por la misma razón, S5 debe ocurrir después de S4. [3]
La corrección no se ve afectada si se accede a la memoria después de la emisión de desbloqueo antes de que se complete el desbloqueo o si se accede a la memoria antes de una emisión de bloqueo después de la adquisición de bloqueo. Sin embargo, el código en la sección crítica no se puede emitir antes de que se complete la adquisición de bloqueo porque es posible que no se garantice la exclusión mutua.
La sincronización posterior a la espera es otra forma de implementación de la coherencia de la versión. Como se muestra en el código de la derecha, se puede garantizar la corrección si las operaciones posteriores se producen solo después de que se completen todos los accesos a la memoria, especialmente el almacenamiento en 'a'. Aparte de eso, la operación de lectura no se debe ejecutar hasta que se haya completado la operación de espera. S2 actúa como una sincronización de liberación y S3 actúa como una sincronización de adquisición. Por lo tanto, S2 debe evitar que se produzca una ejecución anterior después de ella, y S3 debe evitar que se produzca cualquier ejecución posterior antes de ella. S2 no necesita evitar que se produzca una ejecución posterior antes de ella, del mismo modo, S3 no necesita evitar que se produzca ninguna ejecución anterior después de ella.
La coherencia de la versión diferida es una optimización adicional de la coherencia de la versión. Supone que el hilo que ejecuta un acceso de adquisición no necesita los valores escritos por otros hilos hasta que se haya completado el acceso de adquisición. Por lo tanto, todo el comportamiento de coherencia se puede retrasar y se puede ajustar el tiempo de propagación de la escritura. [4]
Considere los escenarios descritos en la imagen de la derecha. Este caso muestra cuándo se realiza la propagación de escritura en un sistema coherente con la memoria caché basado en el modelo de consistencia de la versión. La variable datum se propaga por completo antes de la propagación de datumIsReady. Pero el valor de datum no es necesario hasta después del acceso de sincronización de adquisición en P1 y se puede propagar junto con datumIsReady sin dañar el resultado del programa.
La segunda imagen muestra lo que sucede cuando se aplica la consistencia de liberación diferida. Teniendo en cuenta este escenario, todos los valores escritos antes de la sincronización de liberación se retrasan y se propagan junto con la propagación del acceso de liberación en sí. Por lo tanto, datum y datumIsReady se propagan juntos en el punto de liberación.
"TreadMarks" [5] es una aplicación real de la consistencia de liberación diferida.
La consistencia de la versión diferida puede superar a la consistencia de la versión en ciertos casos. Si hay un sistema con poco ancho de banda entre procesadores o sufre mucho por los altos costos generales debido a la propagación frecuente de pequeños bloques de datos en comparación con la propagación poco frecuente de grandes bloques de datos, LRC puede ayudar mucho al rendimiento.
Considere un sistema que emplea una abstracción de memoria compartida a nivel de software en lugar de una implementación de hardware real. En este sistema, la propagación de escritura se ejecuta con una granularidad de página, lo que hace que sea extremadamente costoso propagar una página completa cuando solo se modifica un bloque de esta página. Por lo tanto, la propagación de escritura se retrasa hasta que se alcanza un punto de sincronización de liberación y en ese momento se modificará toda la página y se propagará toda la página.
LRC requiere la realización de una propagación de escritura en masa en el punto de liberación de la sincronización. La propagación de una cantidad tan grande de escrituras en conjunto ralentizará el acceso de liberación y el acceso de adquisición posterior. Por lo tanto, difícilmente puede mejorar el rendimiento de un sistema de coherencia de caché de hardware.
La consistencia de la versión exige más de los programadores en comparación con el ordenamiento débil. Deben etiquetar los accesos de sincronización como adquisiciones o liberaciones, no solo como accesos de sincronización. De manera similar al ordenamiento débil, la consistencia de la versión permite al compilador reordenar libremente las cargas y los almacenamientos, excepto que no pueden migrar hacia arriba más allá de una sincronización de adquisición y no pueden migrar hacia abajo más allá de una sincronización de liberación. Sin embargo, la ventaja de flexibilidad y rendimiento de la consistencia de la versión se produce a expensas de requerir que los accesos de sincronización se identifiquen correctamente y se identifiquen como adquisiciones y liberaciones. A diferencia del ordenamiento débil, los accesos de sincronización no se pueden identificar fácilmente solo mediante códigos de operación de instrucciones. Por lo tanto, la carga recae sobre los hombros de los programadores para identificar correctamente los accesos de sincronización de adquisición y liberación. [3] [6]
Para la coherencia del procesador, todos los procesos ven las escrituras de cada procesador en el orden en que se iniciaron. Es posible que las escrituras de diferentes procesadores no se vean en el mismo orden, excepto que las escrituras en la misma ubicación se verán en el mismo orden en todas partes. En comparación con la coherencia del procesador, la coherencia de la versión es más relajada porque no impone el orden entre los almacenamientos que ocurre en la coherencia del procesador. No sigue la intuición de los programadores, ya que es relativamente menos restrictiva para las optimizaciones del compilador.