Pregunta

Tengo una clase de bloqueo multi-R / W que mantiene los contadores de lectura, escritura y lectura pendiente, escritura pendiente. Un mutex los protege de múltiples hilos.

Mi pregunta es ¿Todavía necesitamos que los contadores se declaren como volátiles para que el compilador no lo estropee mientras realiza la optimización?

¿O el compilador tiene en cuenta que los contadores están protegidos por el mutex?

Entiendo que el mutex es un mecanismo de tiempo de ejecución para la sincronización y "volátil" La palabra clave es una indicación de tiempo de compilación para que el compilador haga lo correcto mientras realiza las optimizaciones.

Saludos, -Jay.

¿Fue útil?

Solución

Aquí hay 2 elementos básicamente no relacionados, que siempre están confundidos.

  • volátil
  • hilos, bloqueos, barreras de memoria, etc.

volatile se usa para indicarle al compilador que produzca código para leer la variable desde la memoria, no desde un registro. Y para no reordenar el código. En general, no para optimizar o tomar 'atajos'.

Las

barreras de memoria (proporcionadas por mutexes, bloqueos, etc.), como se cita de Herb Sutter en otra respuesta, son para evitar que la CPU reordene las solicitudes de memoria de lectura / escritura, independientemente de cómo dijo el compilador para hacerlo. es decir, no optimice, no tome atajos, a nivel de CPU.

Similar, pero de hecho cosas muy diferentes.

En su caso, y en la mayoría de los casos de bloqueo, la razón por la que NO es volátil es porque se realizan llamadas de función en aras del bloqueo. es decir:

Llamadas a funciones normales que afectan las optimizaciones:

external void library_func(); // from some external library

global int x;

int f()
{
   x = 2;
   library_func();
   return x; // x is reloaded because it may have changed
}

a menos que el compilador pueda examinar library_func () y determinar que no toca x, volverá a leer x en el retorno. Esto es incluso SIN volátil.

Subprocesos:

int f(SomeObject & obj)
{
   int temp1;
   int temp2;
   int temp3;

   int temp1 = obj.x;

   lock(obj.mutex); // really should use RAII
      temp2 = obj.x;
      temp3 = obj.x;
   unlock(obj.mutex);

   return temp;
}

Después de leer obj.x para temp1, el compilador volverá a leer obj.x para temp2, NO por la magia de los bloqueos, sino porque no está seguro si lock () modificó el obj. Probablemente podría establecer indicadores de compilación para optimizar agresivamente (sin alias, etc.) y, por lo tanto, no volver a leer x, pero luego un montón de su código probablemente comenzaría a fallar.

Para temp3, el compilador (con suerte) no volverá a leer obj.x. Si por alguna razón obj.x pudiera cambiar entre temp2 y temp3, entonces usaría volátil (y su bloqueo se rompería / sería inútil).

Por último, si las funciones de bloqueo () / desbloqueo () estuvieran alineadas de alguna manera, tal vez el compilador podría evaluar el código y ver que obj.x no cambia. Pero te garantizo una de dos cosas aquí:   - el código en línea eventualmente llama a alguna función de bloqueo de nivel del sistema operativo (evitando así la evaluación) o   - llama a algunas instrucciones de barrera de memoria asm (es decir, que están envueltas en funciones en línea como __InterlockedCompareExchange) que su compilador reconocerá y evitará reordenarlas.

EDITAR: P.S. Olvidé mencionar: para cosas de pthreads, algunos compiladores están marcados como "compatibles con POSIX" lo que significa, entre otras cosas, que reconocerán las funciones pthread_ y no harán malas optimizaciones a su alrededor. es decir, aunque el estándar C ++ aún no menciona subprocesos, esos compiladores sí (al menos mínimamente).

Entonces, respuesta corta

no necesitas volátil.

Otros consejos

Del artículo de Herb Sutter "Use secciones críticas (preferiblemente bloqueos) para eliminar las carreras" ( http://www.ddj.com/cpp/201804238 ):

  

Entonces, para que una transformación de reordenamiento sea válida, debe respetar las secciones críticas del programa obedeciendo la regla clave de las secciones críticas: el código no puede salir de una sección crítica. (Siempre está bien que el código se mueva). Aplicamos esta regla de oro al exigir una semántica simétrica de cerca unidireccional para el comienzo y el final de cualquier sección crítica, ilustrada por las flechas en la Figura 1:

     
      
  • Entrar en una sección crítica es una operación de adquisición, o una cerca de adquisición implícita: el código nunca puede cruzar la cerca hacia arriba, es decir, moverse desde una ubicación original después de la cerca para ejecutarse antes de la cerca. Sin embargo, el código que aparece antes de la cerca en el orden del código fuente puede cruzar la cerca hacia abajo para ejecutarlo más tarde.
  •   
  • Salir de una sección crítica es una operación de liberación o una valla de liberación implícita: este es solo el requisito inverso de que el código no puede cruzar la valla hacia abajo, solo hacia arriba. Garantiza que cualquier otro hilo que vea la escritura de la versión final también verá todas las escrituras anteriores.
  •   

Entonces, para que un compilador produzca el código correcto para una plataforma de destino, cuando se ingresa y sale una sección crítica (y el término sección crítica se usa en su sentido genérico, no necesariamente en el sentido Win32 de algo protegido por un código < > Estructura CRITICAL_SECTION : la sección crítica puede protegerse con otros objetos de sincronización) se debe seguir la semántica de adquisición y liberación correcta. Por lo tanto, no debería tener que marcar las variables compartidas como volátiles siempre que se acceda a ellas dentro de las secciones críticas protegidas.

volatile se usa para informar al optimizador que siempre cargue el valor actual de la ubicación, en lugar de cargarlo en un registro y asumir que no cambiará. Esto es más valioso cuando se trabaja con ubicaciones de memoria de doble puerto o ubicaciones que se pueden actualizar en tiempo real desde fuentes externas al hilo.

El mutex es un mecanismo de sistema operativo en tiempo de ejecución del que el compilador realmente no sabe nada, por lo que el optimizador no lo tendría en cuenta. Evitará que más de un hilo acceda a los contadores al mismo tiempo, pero los valores de esos contadores aún están sujetos a cambios incluso mientras el mutex está en vigencia.

Entonces, estás marcando los vars como volátiles porque pueden modificarse externamente, y no porque estén dentro de un protector de mutex.

Mantenlos volátiles.

Si bien esto puede depender de la biblioteca de subprocesos que esté utilizando, entiendo que cualquier biblioteca decente no requerirá el uso de volátil .

En Pthreads, por ejemplo , el uso de un mutex asegurará que sus datos se confirmen correctamente en la memoria.

EDITAR: por la presente apruebo la respuesta de tony como mejor que la mía.

Todavía necesita el " volátil " palabra clave.

Los mutexes impiden que los contadores tengan acceso concurrente.

" volátil " le dice al compilador que realmente use el contador en lugar de almacenarlo en caché en un registro de CPU (que no ser actualizado por el hilo concurrente).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top