utiliza solucion sistemas semaforos sección seccion recurso qué proceso problemas peterson para operativos modelado los lectores escritores entrar ejemplos disponible disminuyendo crítica critica corre concurrencia antes algoritmo c++ multithreading concurrency locking

c++ - solucion - ¿Los puntos de secuencia impiden que el código se reordene a través de los límites de la sección crítica?



semaforos sistemas operativos (5)

En resumen, al compilador se le permite reordenar o transformar el programa a su gusto, siempre que el comportamiento observable en una máquina virtual C ++ no cambie. El estándar C ++ no tiene concepto de subprocesos, por lo que esta máquina virtual ficticia solo ejecuta un único subproceso. Y en una máquina tan imaginaria, no tenemos que preocuparnos por lo que ven otros hilos. Mientras los cambios no alteren el resultado del hilo actual , todas las transformaciones de código son válidas, incluida la reordenación de los accesos de memoria a través de puntos de secuencia.

entender por qué la volatilidad no parece ser un requisito para los datos protegidos por mutex realmente es parte de esta pregunta

La volatilidad garantiza una cosa, y una sola cosa: las lecturas de una variable volátil se leerán de la memoria cada vez que el compilador no asumirá que el valor se puede almacenar en caché en un registro. Y así mismo, las escrituras se escribirán a través de la memoria. El compilador no lo guardará en un registro "por un tiempo, antes de escribirlo en la memoria".

Pero eso es todo. Cuando se produce la escritura, se realizará una escritura, y cuando se produce la lectura, se realizará una lectura. Pero no garantiza nada sobre cuándo tendrá lugar esta lectura / escritura. El compilador puede, como suele hacer, reordenar las operaciones como le parezca (siempre que no cambie el comportamiento observable en el subproceso actual, el que conoce la CPU imaginaria de C ++). Tan volátil realmente no resuelve el problema. Por otro lado, ofrece una garantía que realmente no necesitamos. No necesitamos que cada escritura en la variable se escriba inmediatamente, solo queremos asegurarnos de que se escriban antes de cruzar este límite. Está bien si se almacenan en caché hasta entonces, y de la misma manera, una vez que hemos cruzado el límite de la sección crítica, las escrituras posteriores se pueden volver a almacenar en caché por todo lo que nos importa, hasta que crucemos el límite la próxima vez. Por lo tanto, volatile ofrece una garantía demasiado fuerte que no necesitamos, pero no la que necesitamos (que las lecturas / escrituras no se reordenarán)

Entonces, para implementar secciones críticas, necesitamos confiar en la compilación mágica. Tenemos que decirle que "ok, olvídate del estándar de C ++ por un momento, no me importa qué optimizaciones habría permitido si hubieras seguido estrictamente eso. NO debes reordenar ningún acceso de memoria a través de este límite".

Las secciones críticas generalmente se implementan a través de compiladores especiales intrínsecos (esencialmente funciones especiales que son entendidas por el compilador), que 1) obligan al compilador a evitar la reordenación a través de ese intrínseco, y 2) hace que emita las instrucciones necesarias para que la CPU respete el mismo límite (debido a que la CPU también reordena las instrucciones, y sin emitir una instrucción de barrera de memoria, corremos el riesgo de que la CPU haga el mismo reordenamiento que solo evitamos que lo haga el compilador)

Supongamos que uno tiene algún código basado en el bloqueo como el siguiente, donde se utilizan las exclusiones mutuas para protegerse contra la lectura y escritura concurrentes inapropiadas

mutex.get() ; // get a lock. T localVar = pSharedMem->v ; // read something pSharedMem->w = blah ; // write something. pSharedMem->z++ ; // read and write something. mutex.release() ; // release the lock.

Si se supone que el código generado se creó en el orden del programa, todavía hay un requisito para las barreras de memoria de hardware adecuadas como isync, lwsync, .acq, .rel. Asumiré para esta pregunta que la implementación de mutex se encarga de esta parte, proporcionando una garantía de que el pSharedMem lee y escribe todo lo que ocurre "después de" la obtención, y "antes" del lanzamiento () [pero que las lecturas y escrituras circundantes pueden entrar en la sección crítica como espero es la norma para las implementaciones de mutex]. También supondré que los accesos volátiles se utilizan en la implementación de exclusión mutua cuando corresponda, pero que la volatilidad NO se utiliza para los datos protegidos por la exclusión mutua (entender por qué la volatilidad no es un requisito para la protección mutex. Los datos son realmente parte de esta pregunta).

Me gustaría entender qué impide que el compilador mueva los accesos pSharedMem fuera de la región crítica. En los estándares C y C ++ veo que hay un concepto de punto de secuencia. La mayor parte del texto de puntos de secuencia en los documentos de normas me pareció incomprensible, pero si tuviera que adivinar de qué se trataba, es una declaración de que el código no debe reordenarse en un punto donde hay una llamada con efectos secundarios desconocidos. ¿Es esa la esencia de esto? Si ese es el caso, ¿qué tipo de libertad de optimización tiene el compilador aquí?

Con los compiladores realizando complicadas optimizaciones, como la alineación interprocedural impulsada por perfil (incluso a través de los límites de archivo), incluso el concepto de efecto secundario desconocido se vuelve un poco borroso.

Quizás esté más allá del alcance de una pregunta simple explicar esto de manera autónoma aquí, por lo que estoy abierto a ser señalado en referencias (preferiblemente en línea y dirigido a programadores mortales, no a escritores de compiladores y diseñadores de idiomas).

EDITAR: (en respuesta a la respuesta de Jalf)

Mencioné las instrucciones de barrera de memoria como lwsync e isync debido a los problemas de reordenación de la CPU que también mencionaste. Resulta que trabajo en el mismo laboratorio que los compiladores (al menos para una de nuestras plataformas), y habiendo hablado con los implementadores de los intrínsecos, sé que al menos para el compilador xlC __isync () y __lwsync () ( y el resto de los intrínsecos atómicos) también son una barrera de reordenación de código. En nuestra implementación de spinlock, esto es visible para el compilador, ya que esta parte de nuestra sección crítica está en línea.

Sin embargo, supongamos que no estaba usando una implementación de bloqueo de compilación personalizada (como es probable que sea, lo que es poco común), y se llama una interfaz genérica como pthread_mutex_lock (). Allí el compilador no se informa nada más que el prototipo. Nunca lo he visto sugerir que el código no sería funcional

pthread_mutex_lock( &m ) ; pSharedMem->someNonVolatileVar++ ; pthread_mutex_unlock( &m ) ; pthread_mutex_lock( &m ) ; pSharedMem->someNonVolatileVar++ ; pthread_mutex_unlock( &m ) ;

sería no funcional a menos que la variable fuera cambiada a volátil. Ese incremento tendrá una secuencia de carga / incremento / almacenamiento en cada uno de los bloques de código back to back, y no funcionará correctamente si el valor del primer incremento se mantiene en el registro para el segundo.

Parece probable que los efectos secundarios desconocidos de pthread_mutex_lock () sean los que evitan que este ejemplo de incremento de espalda a espalda se comporte de forma incorrecta.

Me estoy convenciendo de que la semántica de una secuencia de código como esta en un entorno de subprocesos no está cubierta estrictamente por las especificaciones de lenguaje C o C ++.


Los puntos de secuencia de C / C ++ se producen, por ejemplo, cuando '';'' se encuentra En qué punto deben ocurrir todos los efectos secundarios de todas las operaciones que lo precedieron. Sin embargo, estoy bastante seguro de que por "efecto secundario" se entiende las operaciones que son parte del lenguaje en sí (como si se incrementara z en ''z ++'') y no los efectos en niveles más altos / bajos (como lo que hace el sistema operativo con respecto a la administración de memoria, administración de hilos, etc. después de que se complete una operación).

¿Eso responde un poco a tu pregunta? Mi punto es realmente que AFAIK, el concepto de puntos de secuencia no tiene nada que ver con los efectos secundarios a los que se refiere.

hth


No, los puntos de secuencia no impiden la reorganización de las operaciones. La regla principal y más amplia que gobierna las optimizaciones es el requisito impuesto sobre el llamado comportamiento observable . El comportamiento observable, por definición, es el acceso de lectura / escritura a volatile variables volatile y las llamadas a las funciones de E / S de la biblioteca. Estos eventos deben ocurrir en el mismo orden y producir los mismos resultados que lo harían en el programa ejecutado "canónicamente". El compilador puede reorganizar y optimizar todo lo demás de forma totalmente libre, de la forma que crea conveniente, ignorando por completo los ordenamientos impuestos por los puntos de secuencia.

Por supuesto, la mayoría de los compiladores están tratando de no hacer reordenamientos excesivamente salvajes. Sin embargo, el problema que está mencionando se ha convertido en un problema práctico real con los compiladores modernos en los últimos años. Muchas implementaciones ofrecen mecanismos adicionales específicos de la implementación que permiten al usuario pedirle al compilador que no cruce ciertos límites al realizar reordenamientos optimizados.

Dado que, como está diciendo, los datos protegidos no se declaran volatile , formalmente hablando, el acceso se puede mover fuera de la región protegida. Si declara que los datos son volatile , debería evitar que esto suceda (suponiendo que el acceso mutex también sea volatile ).


Veamos el siguiente ejemplo:

my_pthread_mutex_lock( &m ) ; someNonVolatileGlobalVar++ ; my_pthread_mutex_unlock( &m ) ;

La función my_pthread_mutex_lock () solo llama a pthread_mutex_lock (). Al usar my_pthread_mutex_lock (), estoy seguro de que el compilador no sabe que es una función de sincronización. Para el compilador, es solo una función, y para mí, es una función de sincronización que puedo reimplementar fácilmente. Debido a que someNonVolatileGlobalVar es global, esperaba que el compilador no mueva someNonVolatileGlobalVar ++ fuera de la sección crítica. De hecho, debido al comportamiento observable , incluso en una situación de un solo hilo, el compilador no sabe si la función anterior y la siguiente a esta instrucción están modificando la var global. Por lo tanto, para mantener el comportamiento observable correcto, debe mantener el orden de ejecución tal como está escrito. Espero que pthread_mutex_lock () y pthread_mutex_unlock () también realicen barreras de memoria de hardware , para evitar que el hardware mueva esta instrucción fuera de la sección crítica.

Estoy en lo cierto?

Si escribo:

my_pthread_mutex_lock( &m ) ; someNonVolatileGlobalVar1++ ; someNonVolatileGlobalVar2++ ; my_pthread_mutex_unlock( &m ) ;

No puedo saber cuál de las dos variables se incrementa primero, pero esto normalmente no es un problema.

Ahora, si escribo:

someGlobalPointer = &someNonVolatileLocalVar; my_pthread_mutex_lock( &m ) ; someNonVolatileLocalVar++ ; my_pthread_mutex_unlock( &m ) ;

o

someLocalPointer = &someNonVolatileGlobalVar; my_pthread_mutex_lock( &m ) ; (*someLocalPointer)++ ; my_pthread_mutex_unlock( &m ) ;

¿El compilador está haciendo lo que un ingenuo desarrollador espera?


ver algo en [linux-kernel] /Documentation/memory-barriers.txt