Cómo asegurar que las variables se almacenen en la memoria antes de que diferentes subprocesos las lean [cerrado]

ACTUALIZACIÓN: Hice esta pregunta de otra forma (ver más abajo) y se cerró por no ser constructiva. Es una pena ya que las respuestas respondieron exactamente a lo que pregunté (y resolvieron mi problema), pero soy nuevo aquí, así que intentaré nuevamente hacerlo más constructivo.

Estoy trabajando en VC ++, en Windows 7. Mi programa de subprocesos múltiples asigna valores a las variables en un subproceso, luego envía una señal a través de un objeto de evento a un subproceso diferente que está bloqueado, esperando esa señal. Debido a cosas como las optimizaciones aportadas por el compilador, no hay garantía de que los datos asignados a una variable por un subproceso estén realmente disponibles para el otro subproceso, incluso si uno está seguro (a través del mecanismo de bloqueo) de que el otro subproceso no intentará acceso hasta un tiempo después de que los datos hayan sido asignados a la variable. Por ejemplo, el valor puede estar en un registro de la CPU y permanecer allí hasta que se necesite ese registro para otra cosa. Esto puede evitar cargas innecesarias de la memoria si el valor se necesita nuevamente poco después de que se haya ingresado en ese registro. Desafortunadamente, eso significa que la ubicación correspondiente en la memoria continúa manteniendo el último valor que tenía antes de que se le asigne el nuevo valor. Así, cuando el otro hilo se desbloquee y acceda a la memoria que contiene el valor de la variable, obtendrá el los ancianos valor, no el asignado más recientemente.

La pregunta, entonces, es: ¿Cómo un subproceso de Windows impone el almacenamiento en la memoria de los valores que asigna a las variables, de modo que otro subproceso tenga acceso seguro a ellos en un momento posterior? Puede haber varias respuestas, pero la ofrecida antes de que se cerrara esta pregunta que parecía ser la mejor opción para lo que necesitaba era el uso de una "cerca de memoria", que era una construcción de programación de la que no había oído hablar anteriormente. Una vez que se encuentra la valla, se garantiza que se completaron las escrituras pendientes en la memoria. (Eso es si la valla es una valla de "escritura"; se puede forzar una lectura de la memoria con una valla de "lectura" y se pueden hacer ambas cosas con una valla de "lectura/escritura". Windows hace que las tres estén disponibles con bastante facilidad dentro de un VC++ programa.)

Un pequeño problema resultó ser que las vallas de Windows (también conocidas como "barreras de memoria") solo aplican sus garantías al almacenamiento global, no local (por las razones explicadas en las páginas de MSDN correspondientes).

Si mi interpretación aquí de cómo funcionan las vallas de memoria es incorrecta (y los moderadores vuelven a abrir esta pregunta), me complacería ver que se explica en los comentarios. No preguntaría si no fuera lo suficientemente humilde como para admitir que no lo sabía, después de todo. (Si los moderadores no lo vuelven a abrir, pero puede ver que tengo algo mal, envíeme un correo electrónico y hágamelo saber; estaré encantado de ayudar a mantener viva esta discusión en mi blog, si lo haces.)

VERSIÓN ORIGINAL
¿Cuál es una buena manera de compartir datos entre subprocesos?

Pedí una pregunta antes sobre volatile variables que me abrieron una enorme experiencia de aprendizaje. Entre otras cosas, me di cuenta de que no estaba haciendo la pregunta correcta. Espero que esto no sea una mala etiqueta de stackoverflow, pero creo que debería crear una nueva pregunta aquí que aborde mi problema subyacente:

Tengo dos subprocesos, A y B, en mi programa de Visual C++. B está bloqueado, esperando una señal de A. A establece una serie de variables. Luego, A señala a B, que leerá las variables establecidas por A. Me preocupa que algunas de las variables establecidas por A no se vuelvan a escribir en la memoria, ya que solo pueden residir en los registros de la CPU.

¿Cuál es una buena manera de asegurarse de que el subproceso B, al leer las variables establecidas previamente por el subproceso A, leerá los valores que el subproceso A estableció?

preguntado el 22 de mayo de 12 a las 17:05

Creo que encontrará que, en esencia, esto es de lo que se trata el subprocesamiento múltiple. ¿Cómo se gestionan los datos compartidos entre subprocesos? -

Deberías mirar hacia arriba IPC (comunicación entre procesos)... -

IPC no tiene nada que ver con la comunicación entre subprocesos. -

@Spencer: Si quiere decir, "¿Cómo sincronizo el acceso para evitar condiciones de carrera?" Creo que lo manejé haciendo que cada hilo se bloquee mientras esperaba una señal del otro. Mi problema particular es poder garantizar que, una vez desbloqueado, un hilo realmente tendrá acceso a los valores escritos en ubicaciones compartidas por el otro hilo. -

@Stevens: ¿Qué mecanismo de bloqueo estás usando? Si es una sección crítica o mutex, entonces ya está listo, ya que tienen barreras de memoria implícitas en Windows. -

4 Respuestas

Bajo una arquitectura x86 no hay mucho de qué preocuparse cuando se utiliza una buena biblioteca.

Proteja el acceso a los datos compartidos usando exclusiones mutuas (por ejemplo, boost::mutex) y si el implementador de la exclusión mutua lo hizo bien, habrá usado una barrera de memoria (Barreras de memoria @ MSDN) para asegurarse de que los cachés se hayan vaciado en la memoria.

Si tuviera que escribir su propio código de sincronización, agréguele barreras de memoria.

contestado el 22 de mayo de 12 a las 18:05

Eso suena prometedor, pero mi primer escaneo rápido sugiere que esos mecanismos son para lidiar con la atomicidad y el reordenamiento. Creo que mis métodos de sincronización ya están solucionando esos problemas. - stevens miller

Aceptando esta respuesta ya que fue la primera en mencionar las barreras de la memoria, pero también gracias y un voto positivo a Mehrdad por su respuesta. - stevens miller

Si estás usando mutices, ya has diseñado mal X_X - djechlin

@jjechlin: Como una contracción de EXclusión MUTual forzada en un sustantivo, el plural de mutex es mutex. mutices solo sería correcto si la palabra tuviera una derivación latina directa terminada en -ex... y es muy feo ;-) - Clifford

@StevensMiller El documento es un poco malo, pero void MemoryBarrier(void); es un comando del procesador, que asegura que accesos a la memoria no se reordenan y los cachés se vacían. - cristobal oezbek

Mencionaste en un comentario, Mi problema particular es poder garantizar que, una vez desbloqueado, un hilo tendrá acceso a los valores escritos en ubicaciones compartidas por el otro hilo..

Creo que la respuesta a tu pregunta es simple: puedes usar _ReadWriteBarrier() (o, en su caso particular, probablemente solo _WriteBarrier dentro de los subprocesos de lectura servirá) para asegurarse de leer valores de memoria actualizados.

Tenga en cuenta que, hasta donde yo sé, en C/C++, volatile is no garantizado que tenga cualquier semántica de barrera de memoria, por lo que no puede simplemente usar volatile en esos idiomas. Las barreras de memoria son el camino a seguir para simplemente leer valores actualizados.

contestado el 22 de mayo de 12 a las 18:05

Sí, lo fue volatile eso me metió en esta solución en primer lugar. Si bien parece garantizar un grado de certeza con respecto a las variables que se escriben en la memoria con cada cambio (y, Por favor que nadie salte sobre mi garganta por eso; ya me he dado cuenta de eso volatile es un tema delicado 8-) ), no es una solución al problema general de saber si los valores establecidos por un hilo antes de que ese hilo desbloquee a otro estarán disponibles para el hilo desbloqueado. Gracias por la pista sobre las barreras de la memoria. Estudiaré más sobre eso. - stevens miller

@StevensMiller: ¡Claro! También, este hilo podría tener una mejor explicación que la mía. - user541686

Ese hilo dice: "Las barreras de memoria también aseguran que todas las lecturas/escrituras pendientes se ejecuten cuando se alcanza la barrera, por lo que efectivamente nos brinda todo lo que necesitamos por sí mismo, lo que hace que volátil sea innecesario. Simplemente podemos eliminar el calificador volátil por completo". Eso suena como una solución, si, por "lecturas/escrituras pendientes", significa lecturas/escrituras desde/hacia memoria. ¿Eso es lo que significa? Buscaré en el MSDN y veré si puedo confirmarlo. Gracias de nuevo. - stevens miller

@StevensMiller: Sí, eso es lo que significa: los registros no se ven afectados. - user541686

¡Eso parece haber funcionado! He tirado todo el uso de volatile y simplemente agregado _WriteBarrier() antes de señalar el hilo bloqueado. Ahora, mencionaste agregar _WriteBarrier() dentro de lectura hilos. Ese hilo no tiene escrituras pendientes. ¿Seguiría funcionando allí? ¿Funcionaría sin importar en qué hilo lo puse? Gracias por toda la ayuda. ¡He aprendido mucho hoy! - stevens miller

Esto es como preguntar "cuál es una buena manera de escribir un programa orientado a objetos". Excepto a esa pregunta, diría "ve a leer un buen libro", pero a esta, realmente no hay un buen libro sobre un mal paradigma. Una gran cantidad de programación multiproceso se basa en minimizando uso de datos compartidos en lugar de usarlos bien.

Entonces, mis sugerencias son:

1) Diseñe de modo que no sea necesario que dos subprocesos se comuniquen entre sí de esta manera. Suena más a un hilo de procedimiento que a dos hilos verdaderamente independientes.

2) Implemente una arquitectura orientada a servicios dentro de su proceso o entre procesos. Haga que todos los datos compartidos ocurran sobre patrones efímeros de solicitud/respuesta en lugar de confiar en el uso de variables globales que se sondean. Todas estas variables que A establece y le dice a B que lea suenan mucho como una "solicitud" que el "cliente" A está enviando al "servidor" B.

3) Si está de acuerdo con instalar y esforzarse en aprender una biblioteca, le recomiendo ZMQ. He tenido una buena experiencia con él y anuncian (y según mi experiencia entregan) su herramienta, que en el servicio parece una biblioteca para implementar clientes y servidores, como una forma de deshacerse de todos los datos compartidos entre subprocesos. Si nada más, la documentación podría brindarle buenas maneras de pensar en cobrar sus datos compartidos entre subprocesos para patrones que no los involucren.

contestado el 22 de mayo de 12 a las 18:05

Lo entiendo, pero traté de ser específico sobre dos subprocesos sincronizados y el problema particular de asegurarse de que los valores almacenados en caché en los registros se escriban en la memoria antes de desbloquear un subproceso en espera. creo que eso es un pequeño menos vago "¿cuál es una buena manera de escribir un programa orientado a objetos?" - stevens miller

WRT #1: Tienes razón, pero el hilo con el que me estoy comunicando se crea mediante una llamada a la API de Windows. No puedo diseñar eso fuera de mi programa. #2: Mis globales no son sondeados; Sincronizo los hilos con cada uno alternativamente esperando que el otro le diga que se desbloquee. #3: Me encanta una buena biblioteca, pero no puedo rediseñar la estructura del sistema con el que estoy trabajando. - stevens miller

Está describiendo un patrón de solicitud/respuesta. Los datos compartidos son una solicitud en una dirección y una respuesta en otra. El comando "desbloquear" es cuando está enviando la solicitud, y el "bloquear" está escuchando una solicitud. Así es como lo implementaría, de modo que todos los datos compartidos tengan una vida útil limitada y un alcance limitado. - djechlin

Eso suena como una coincidencia con lo que estoy haciendo. La vida útil limitada y los alcances no son un problema, afaik. Obtuve una referencia o un indicador para mí sobre cómo implementar el patrón de una manera que garantice que las lecturas/escrituras en los datos compartidos serán todas desde/hacia las mismas ubicaciones (es decir, eso evitará el problema de almacenamiento en caché). me dirijo)? - stevens miller

Si entiendo bien, la forma más sencilla de hacerlo es una solicitud global de RequestFromA* a_req; variable a la que B accederá para encontrar los datos de A para usar en la solicitud. El problema es que esto solo funciona si hay exactamente uno de A y uno de B. Sin embargo, el patrón que desea es el "patrón de mediador" (consulte Patrones de diseño o Google). djechlin

Si quiere decir, "¿Cómo sincronizo el acceso para evitar condiciones de carrera?" Creo que lo manejé haciendo que cada hilo se bloquee mientras esperaba una señal del otro. Mi problema particular es poder garantizar que, una vez desbloqueado, un hilo realmente tendrá acceso a los valores escritos en ubicaciones compartidas por el otro hilo.

Sí exactamente. El problema es que esperar una señal establecida por algún subproceso no es suficiente para garantizar que cualquiera de las otras actividades de ese subproceso sea visible desde el subproceso actual. Un subproceso puede establecer una variable, activar la señal y luego un subproceso que espera la señal puede acceder a la variable, pero obtener un valor completamente diferente.

Actualmente estoy disfrutando anthony williams libro, Concurrencia de C ++ en acción, sobre este tema. La respuesta parece estar en el uso correcto de las órdenes de memoria std::atomic. Aquí hay un ejemplo:

std::atomic<bool> signal(false);
std::atomic<int> i(0);

-- thread 1 --
i.store(100,std::memory_order_relaxed);
signal.store(true,std::memory_order_release);

-- thread 2 --
while(!signal.load(std::memory_order_acquire));
assert(i.load(std::memory_order_relaxed) == 100);

Cuando el segundo hilo ve la señal, se establece una relación entre el almacenamiento realizado con memory_order_release y la carga realizada con memory_order_acquire lo que garantiza que el almacenamiento para i será visible en el segundo hilo. Por lo tanto, se garantiza que la afirmación se mantenga.

Por otro lado, si usa órdenes de memoria menos estrictas, no obtendrá ninguna garantía.

-- thread 1 --
i.store(100,std::memory_order_relaxed);
signal.store(true,std::memory_order_relaxed);

-- thread 2 --
while(!signal.load(std::memory_order_relaxed));
int i2 = i.load(memory_order_relaxed);
// No guarantees about the value loaded from i!

Alternativamente, puede usar el orden de memoria predeterminado que garantiza la coherencia secuencial siempre que no tenga carreras de datos.

std::atomic<bool> signal(false);
int i = 0;

-- thread 1 --
i = 100;
signal = true;

-- thread 2 --
while(!signal);
assert(i == 100);

contestado el 23 de mayo de 17 a las 13:05

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.