¿Es la función llamada una barrera de memoria efectiva para las plataformas modernas?
multithreading memory-barriers (4)
En la práctica, él es correcto y una barrera de memoria está implícita en este caso específico.
Pero el punto es que si su presencia es "discutible", el código ya es demasiado complejo y poco claro.
Realmente chicos, usen mutex u otras construcciones apropiadas. Es la única forma segura de manejar los hilos y escribir código que se pueda mantener.
Y tal vez verá otros errores, como que el código es impredecible si se llama a send () más de una vez.
En una base de código que revisé, encontré el siguiente modismo.
void notify(struct actor_t act) {
write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
global.data = data;
notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
case ''M'': use_data(global.data);break;
...
}
"Espera", le dije al autor, un miembro global.data
de mi equipo, "¡no hay barrera de memoria aquí! No garantizas que los global.data
se global.data
de la caché a la memoria principal. Si el hilo A y el hilo B se ejecutará en dos procesadores diferentes: este esquema podría fallar ".
El programador senior sonrió, y explicó lentamente, como si explicara a su niño de cinco años cómo atarle los cordones de sus zapatos: "Escucha chico, hemos visto aquí muchos errores relacionados con el hilo, en pruebas de carga alta, y en clientes reales", dijo. hizo una pausa para rascarse la larga barba, "pero nunca hemos tenido un error con este modismo".
"Pero, dice en el libro ..."
"¡Silencio!", Me calló rápidamente, "Tal vez teóricamente, no está garantizado, pero en la práctica, el hecho de que utilizó una llamada a función es efectivamente una barrera de memoria. El compilador no reordenará la instrucción global.data = data
, ya que no se puede saber si alguien lo está usando en la llamada a la función, y la arquitectura x86 asegurará que las otras CPU verán esta información global en el momento en que el hilo B lea el comando de la tubería. Tenga la seguridad de que tenemos un amplio mundo real problemas de los que preocuparse. No necesitamos invertir un esfuerzo extra en problemas teóricos falsos.
"Tranquilízate, muchacho, con el tiempo entenderás que separe el problema real de los que no necesitan problemas para obtener un doctorado".
¿Está correcto? ¿Realmente eso no es un problema en la práctica (digamos x86, x64 y ARM)?
Va en contra de todo lo que aprendí, ¡pero tiene una barba larga y un aspecto realmente inteligente!
¡Puntos extra si me puedes mostrar un código que demuestre que está equivocado!
En la práctica, una llamada de función es una barrera de compilación , lo que significa que el compilador no moverá los accesos de memoria global más allá de la llamada. Una advertencia sobre esto es sobre funciones de las cuales el compilador sabe algo, por ejemplo, funciones integradas (tenga en cuenta IPO), etc.
Por lo tanto, en teoría se necesita una barrera de memoria del procesador (además de una barrera compiladora) para que esto funcione. Sin embargo, dado que estás llamando a lectura y escritura, que son llamadas de sistema que cambian el estado global, estoy bastante seguro de que el núcleo emite barreras de memoria en algún lugar de la implementación de esas. Sin embargo, no hay tal garantía, por lo que en teoría necesita las barreras.
La regla básica es: el compilador debe hacer que el estado global parezca exactamente como lo codificó, pero si puede demostrar que una función dada no usa variables globales, entonces puede implementar el algoritmo de la forma que prefiera.
El resultado es que los compiladores tradicionales siempre trataban las funciones en otra unidad de compilación como una barrera de memoria porque no podían ver dentro de esas funciones. Cada vez más, los compiladores modernos están desarrollando estrategias de optimización de "todo el programa" o "tiempo de enlace" que rompen estas barreras y harán que el código mal escrito falle, a pesar de que ha estado funcionando bien durante años.
Si la función en cuestión está en una biblioteca compartida, no podrá verla dentro, pero si la función es una definida por el estándar C, entonces no necesita hacerlo, ya sabe lo que hace la función. - Así que debes tener cuidado con esos también. Tenga en cuenta que un compilador no reconocerá una llamada del núcleo por lo que es, sino que el solo acto de insertar algo que el compilador no puede reconocer (ensamblador en línea o una llamada de función a un archivo ensamblador) creará una barrera de memoria en sí misma.
En su caso, notify
será una caja negra que el compilador no puede ver adentro (una función de biblioteca) o bien contendrá una barrera de memoria reconocible, por lo que es más probable que esté a salvo.
En la práctica, debes escribir un código muy malo para no caer en esto.
Las barreras de memoria no son solo para evitar el reordenamiento de la instrucción. Incluso si las instrucciones no se reordenan, aún puede causar problemas con la coherencia de la caché. En cuanto a la reordenación, depende de su compilador y configuración. ICC es particularmente agresivo con la reordenación. MSVC con optimización de todo el programa puede serlo también.
Si su variable de datos compartidos se declara como volatile
, aunque no esté en la especificación, la mayoría de los compiladores generarán una variable de memoria alrededor de las lecturas y escrituras de la variable y evitarán el reordenamiento. Esta no es la forma correcta de usar volatile
, ni para qué fue diseñada.
(Si me quedaban votos, haría +1 a su pregunta para la narración).