teoria las fisica cuerdas cuantica c++ windows multithreading locking interlocked

c++ - las - teoria cuantica



Lectura de variables entrelazadas (10)

Asumir:

A. C ++ bajo WIN32.

B. Un entero volátil correctamente alineado incrementado y disminuido usando InterlockedIncrement() y InterlockedDecrement() .

__declspec (align(8)) volatile LONG _ServerState = 0;

Si quiero simplemente leer _ServerState, ¿necesito leer la variable a través de una función InterlockedXXX ?

Por ejemplo, he visto código como:

LONG x = InterlockedExchange(&_ServerState, _ServerState);

y

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

El objetivo es simplemente leer el valor actual de _ServerState .

No puedo simplemente decir:

if (_ServerState == some value) { // blah blah blah }

Parece haber cierta confusión WRT este tema. Entiendo que las lecturas de tamaño de registro son atómicas en Windows, por lo que asumo que la función InterlockedXXX no es necesaria.

Matt J.

Bien, gracias por las respuestas. Por cierto, esto es Visual C ++ 2005 y 2008.

Si es verdad, debería usar una función InterlockedXXX para leer el valor de _ServerState , aunque sea por claridad, ¿cuál es la mejor manera de hacerlo?

LONG x = InterlockedExchange(&_ServerState, _ServerState);

Esto tiene el efecto secundario de modificar el valor, cuando todo lo que realmente quiero hacer es leerlo. No solo eso, sino que existe la posibilidad de que pueda restablecer el indicador al valor incorrecto si hay un cambio de contexto cuando se _ServerState el valor de _ServerState en la pila como preparación para llamar a InterlockedExchange() .

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

Tomé esto de un ejemplo que vi en MSDN.
Consulte http://msdn.microsoft.com/en-us/library/ms686355(VS.85).aspx

Todo lo que necesito es algo a lo largo de las líneas:

lock mov eax, [_ServerState]

En cualquier caso, el punto, que me pareció claro, es proporcionar acceso seguro a subprocesos a una bandera sin incurrir en la sobrecarga de una sección crítica. He visto los LONG utilizados de esta manera a través de la familia de funciones InterlockedXXX() , de ahí mi pregunta.

Bien, estamos pensando que una buena solución a este problema de leer el valor actual es:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);


Depende de lo que quiere decir con "el objetivo es simplemente leer el valor actual de _ServerState" y depende del conjunto de herramientas y la plataforma que utilice (usted especifica Win32 y C ++, pero no qué compilador de C ++, y eso puede ser importante) .

Si simplemente desea leer el valor de tal manera que el valor no esté dañado (es decir, si algún otro procesador cambia el valor de 0x12345678 a 0x87654321, su lectura obtendrá uno de esos 2 valores y no 0x12344321), entonces la lectura será correcta como siempre que la variable sea:

  • marcado volatile ,
  • correctamente alineado, y
  • lea utilizando una sola instrucción con un tamaño de palabra que el procesador maneja de forma atómica

Nada de esto es prometido por el estándar C / C ++, pero Windows y MSVC sí ofrecen estas garantías, y creo que la mayoría de los compiladores que apunta a Win32 también lo hacen.

Sin embargo, si desea que su lectura se sincronice con el comportamiento del otro hilo, hay algo de complejidad adicional. Digamos que tienes un protocolo simple de ''buzón'':

struct mailbox_struct { uint32_t flag; uint32_t data; }; typedef struct mailbox_struct volatile mailbox; // the global - initialized before wither thread starts mailbox mbox = { 0, 0 }; //*************************** // Thread A while (mbox.flag == 0) { /* spin... */ } uint32_t data = mbox.data; //*************************** //*************************** // Thread B mbox.data = some_very_important_value; mbox.flag = 1; //***************************

La idea es que el subproceso A girará a la espera de que mbox.flag indique que mbox.data tiene una información válida. El subproceso B escribirá algunos datos en el buzón.data y luego establecerá mbox.flag en 1 como una señal de que mbox.data es válido.

En este caso, una simple lectura en el subproceso A de mbox.flag puede obtener el valor 1, aunque una lectura posterior de mbox.data en el subproceso A no obtiene el valor escrito por el subproceso B.

Esto se debe a que, aunque el compilador no reordena las escrituras del subproceso B en mbox.data y mbox.flag, el procesador y / o el caché podrían hacerlo. C / C ++ garantiza que el compilador generará código tal que Thread B escribirá en mbox.data antes de escribir en mbox.flag, pero el procesador y el caché pueden tener una idea diferente: un manejo especial llamado "barreras de memoria" o "adquisición y Se debe usar la semántica de liberación para garantizar que los pedidos se realicen por debajo del nivel de la secuencia de instrucciones del hilo.

No estoy seguro si los compiladores que no sean MSVC hacen reclamos sobre pedidos por debajo del nivel de instrucción. Sin embargo, MS garantiza que la volatilidad de MSVC es suficiente - MS especifica que las escrituras volátiles tienen semántica de lanzamiento y las lecturas volátiles han adquirido semántica, aunque no estoy seguro de a qué versión de MSVC se aplica esto, visite http://msdn.microsoft.com/en-us/library/12a04hfd.aspx?ppud=4 .

También he visto un código como el que usted describe que utiliza API enclavadas para realizar lecturas y escrituras simples en ubicaciones compartidas. Mi opinión sobre el tema es utilizar las API entrelazadas. La comunicación entre subprocesos sin bloqueo está llena de trampas muy difíciles de entender y sutiles, y tratar de tomar un atajo en un bit crítico de código que puede terminar con un error muy difícil de diagnosticar no me parece una buena idea . Además, el uso de una API entrelazada transmite a cualquier persona que mantenga el código, "este es el acceso a los datos que debe compartirse o sincronizarse con otra cosa, ¡ pise con cuidado! ".

Además, al usar la API interbloqueada, está sacando de la imagen las características específicas del hardware y el compilador: la plataforma se asegura de que todo eso se resuelva de manera adecuada, sin más dudas ...

Lea los artículos sobre la concurrencia efectiva de Herb Sutter en DDJ (que en este momento están fuera de servicio, al menos para mí) para obtener buena información sobre este tema.


Es posible que la lectura del valor actual no necesite ningún bloqueo.


Las funciones de Enclavamiento * impiden que dos procesadores diferentes accedan a la misma pieza de memoria. En un solo sistema de procesador vas a estar bien. Si tiene un sistema de doble núcleo donde tiene subprocesos en diferentes núcleos que acceden a este valor, es posible que tenga problemas para hacer lo que cree que es atómico sin el Interbloqueado *.


Las instrucciones entrelazadas proporcionan atomicidad y sincronización entre procesadores. Tanto las escrituras como las lecturas deben estar sincronizadas, por lo tanto, sí, debe utilizar instrucciones entrelazadas para leer un valor que se comparte entre subprocesos y no está protegido por un bloqueo. La programación sin bloqueo (y eso es lo que estás haciendo) es un área muy difícil, por lo que podrías considerar usar bloqueos en su lugar. ¿A menos que este sea realmente uno de los cuellos de botella de su programa que deba optimizarse?


Las operaciones de lectura de 32 bits ya son atómicas en algunos sistemas de 32 bits (la especificación de Intel dice que estas operaciones son atómicas, pero no hay garantía de que esto sea cierto en otras plataformas compatibles con x86). Así que no deberías usar esto para la sincronización de hilos.

Si necesita una marca de algún tipo, debería considerar usar el objeto de Event y la función WaitForSingleObject para ese propósito.


Leer está bien. Un valor de 32 bits siempre se lee como un todo mientras no se divide en una línea de caché. Tu align 8 garantiza que siempre esté dentro de una línea de caché, por lo que estarás bien.

Olvídate de reordenar las instrucciones y todo lo que no tiene sentido. Los resultados siempre se retiran en orden. Sería un procesador de memoria de lo contrario !!!

Incluso para una máquina con doble CPU (es decir, compartida a través de los FSB más lentos), todavía estará bien ya que las CPU garantizan la coherencia de la memoria caché a través del Protocolo MESI. Lo único que no tienes garantizado es que el valor que lees puede no ser el último absoluto. PERO, ¿cuál es la última de todos modos? Es algo que probablemente no necesite saber en la mayoría de las situaciones si no está escribiendo en la ubicación basándose en el valor de esa lectura. De lo contrario, habrías usado operaciones entrelazadas para manejarlo en primer lugar.

En resumen, no gana nada al usar operaciones entrelazadas en una lectura (excepto que tal vez le recuerde a la siguiente persona que debe mantener su código con cuidado, por otra parte, es posible que esa persona no esté calificada para mantener su código para comenzar).

EDITAR: En respuesta a un comentario dejado por Adrian McCarthy .

Estás pasando por alto el efecto de las optimizaciones del compilador. Si el compilador piensa que ya tiene el valor en un registro, entonces reutilizará ese valor en lugar de releerlo de la memoria. Además, el compilador puede hacer la reordenación de instrucciones para la optimización si cree que no hay efectos secundarios observables.

No dije que leer desde una variable no volátil está bien. Todo lo que se preguntaba era si se requería un enclavamiento. De hecho, la variable en cuestión fue declarada claramente con volatile . ¿O estaba pasando por alto el efecto de la palabra clave volatile ?


Para cualquier persona que tenga que volver a visitar este hilo, quiero agregar a lo que Bartosz explicó bien que _InterlockedCompareExchange() es una buena alternativa a atomic_load() estándar si la atómica estándar no está disponible. Aquí está el código para leer atómicamente my_uint32_t_var en C en i86 Win64. atomic_load() se incluye como un punto de referencia:

long debug_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var); 00000001401A6955 mov eax,dword ptr [rbp+30h] 00000001401A6958 xor edi,edi 00000001401A695A mov dword ptr [rbp-0Ch],eax debug_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0); 00000001401A695D xor eax,eax 00000001401A695F lock cmpxchg dword ptr [rbp+30h],edi 00000001401A6964 mov dword ptr [rbp-0Ch],eax debug_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0); 00000001401A6967 prefetchw [rbp+30h] 00000001401A696B mov eax,dword ptr [rbp+30h] 00000001401A696E xchg ax,ax 00000001401A6970 mov ecx,eax 00000001401A6972 lock cmpxchg dword ptr [rbp+30h],ecx 00000001401A6977 jne foo+30h (01401A6970h) 00000001401A6979 mov dword ptr [rbp-0Ch],eax long release_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var); 00000001401A6955 mov eax,dword ptr [rbp+30h] release_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0); 00000001401A6958 mov dword ptr [rbp-0Ch],eax 00000001401A695B xor edi,edi 00000001401A695D mov eax,dword ptr [rbp-0Ch] 00000001401A6960 xor eax,eax 00000001401A6962 lock cmpxchg dword ptr [rbp+30h],edi 00000001401A6967 mov dword ptr [rbp-0Ch],eax release_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0); 00000001401A696A prefetchw [rbp+30h] 00000001401A696E mov eax,dword ptr [rbp+30h] 00000001401A6971 mov ecx,eax 00000001401A6973 lock cmpxchg dword ptr [rbp+30h],ecx 00000001401A6978 jne foo+31h (01401A6971h) 00000001401A697A mov dword ptr [rbp-0Ch],eax


Su comprensión inicial es básicamente correcta. De acuerdo con el modelo de memoria que Windows requiere en todas las plataformas MP que admite (o que siempre admitirá), las lecturas de una variable marcada de forma natural y volátil son atómicas siempre que sean más pequeñas que el tamaño de una palabra de máquina. Lo mismo con las escrituras. No necesitas un prefijo de ''bloqueo''.

Si realiza las lecturas sin utilizar un interbloqueo, está sujeto a la reordenación del procesador. Esto incluso puede ocurrir en x86, en una circunstancia limitada: las lecturas de una variable se pueden mover por encima de las escrituras de una variable diferente. En casi todas las arquitecturas no compatibles con x86 que admite Windows, está sujeto a una reordenación aún más complicada si no usa interbloqueos explícitos.

También existe el requisito de que si está utilizando un ciclo de intercambio de comparación, debe marcar la variable en la que se está comparando el intercambio como volátil. Aquí hay un ejemplo de código para demostrar por qué:

long g_var = 0; // not marked ''volatile'' -- this is an error bool foo () { long oldValue; long newValue; long retValue; // (1) Capture the original global value oldValue = g_var; // (2) Compute a new value based on the old value newValue = SomeTransformation(oldValue); // (3) Store the new value if the global value is equal to old? retValue = InterlockedCompareExchange(&g_var, newValue, oldValue); if (retValue == oldValue) { return true; } return false; }

Lo que puede salir mal es que el compilador está dentro de sus derechos para volver a recuperar oldValue de g_var en cualquier momento si no es volátil. Esta optimización de ''rematerialización'' es excelente en muchos casos porque puede evitar derramar registros en la pila cuando la presión del registro es alta.

Así, el paso (3) de la función se convertiría en:

// (3) Incorrectly store new value regardless of whether the global // is equal to old. retValue = InterlockedCompareExchange(&g_var, newValue, g_var);


Tu camino es bueno:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

Estoy usando una solución similar:

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);


deberías estar bien Es volátil, por lo que el optimizador no debería salvarte, y es un valor de 32 bits, por lo que debería ser al menos aproximadamente atómico. La única sorpresa posible es si la línea de instrucciones puede sortear eso.

Por otro lado, ¿cuál es el costo adicional de usar las rutinas protegidas?