c++ - poo - Inicialización segura de subprocesos de objetos const constantes locales de función
metodos en c++ (8)
Esta pregunta me hizo cuestionar una práctica que había estado siguiendo durante años.
Para la inicialización segura de subprocesos de objetos const constantes de función local, protejo la construcción real del objeto, pero no la inicialización de la referencia de función local que se refiere a él. Algo como esto:
namespace {
const some_type& create_const_thingy()
{
lock my_lock(some_mutex);
static const some_type the_const_thingy;
return the_const_thingy;
}
}
void use_const_thingy()
{
static const some_type& the_const_thingy = create_const_thingy();
// use the_const_thingy
}
La idea es que el bloqueo lleva tiempo, y si la referencia se sobrescribe con varios subprocesos, no importará.
Me interesaría si esto es
- ¿Lo suficientemente seguro en la práctica?
- ¿Seguro según las reglas? (Lo sé, el estándar actual ni siquiera sabe qué es la "concurrencia", pero ¿qué hay de pisotear una referencia ya iniciada? ¿Y otros estándares, como POSIX, tienen algo que decir que sea relevante para esto?)
La razón por la que quiero saber esto es porque quiero saber si puedo dejar el código tal como está o si debo regresar y solucionarlo.
Para las mentes inquisitivas:
Muchos de los objetos constantes de esta función local-estática que utilicé son mapas que se inicializan desde matrices const en el primer uso y se utilizan para la búsqueda. Por ejemplo, tengo algunos analizadores XML donde las cadenas de nombres de etiquetas se asignan a valores de enum
, por lo que luego podría switch
los valores de enum
las etiquetas.
Ya que recibí algunas respuestas sobre qué hacer en su lugar, pero no tengo una respuesta a mis preguntas reales (ver 1. y 2. más arriba), comenzaré una recompensa por esto. Otra vez:
No estoy interesado en lo que podría hacer, realmente quiero saber sobre esto .
Aquí está mi toma (si realmente no puedes inicializarla antes de que se inicien los hilos):
He visto (y he usado) algo como esto para proteger la inicialización estática, usando boost :: once
#include <boost/thread/once.hpp>
boost::once_flag flag;
// get thingy
const Thingy & get()
{
static Thingy thingy;
return thingy;
}
// create function
void create()
{
get();
}
void use()
{
// Ensure only one thread get to create first before all other
boost::call_once( &create, flag );
// get a constructed thingy
const Thingy & thingy = get();
// use it
thingy.etc..()
}
En mi entendimiento, de esta manera todos los hilos esperan en boost :: call_once excepto uno que creará la variable estática. Se creará una sola vez y nunca más se volverá a llamar. Y entonces ya no tienes cerradura.
En resumen, creo que:
La inicialización del objeto es segura para la ejecución de subprocesos, asumiendo que "some_mutex" está completamente construido cuando se ingresa "create_const_thingy".
No se garantiza que la inicialización de la referencia de objeto dentro de "use_const_thingy" sea segura para subprocesos; podría (como usted dice) estar sujeto a que se inicialice varias veces (lo cual es un problema menor), pero también podría estar sujeto a un desgarro de palabras que podría resultar en un comportamiento indefinido.
[Supongo que la referencia de C ++ se implementa como una referencia al objeto real utilizando un valor de puntero, que en teoría podría leerse cuando se escribe parcialmente en].
Por lo tanto, para tratar de responder a su pregunta:
Lo suficientemente seguro en la práctica: muy probable, pero en última instancia depende del tamaño del puntero, la arquitectura del procesador y el código generado por el compilador. El punto clave aquí es probablemente si una escritura / lectura de tamaño de puntero es atómica o no.
Seguro de acuerdo con la regla: Bueno, no hay tales reglas en C ++ 98, lo siento (pero eso ya lo sabías).
Actualización: Después de publicar esta respuesta, me di cuenta de que solo se enfoca en una parte pequeña y esotérica del problema real y, debido a esto, decidí publicar otra respuesta en lugar de editar los contenidos. Dejo el contenido tal como está, ya que tiene cierta relevancia para la pregunta (y también para humillarme, recordándome que piense un poco más antes de responder).
Entonces, la parte relevante de la especificación es 6.7 / 4:
Se permite que una implementación realice una inicialización temprana de otros objetos locales con una duración de almacenamiento estática en las mismas condiciones que una implementación tiene permitido inicializar de forma estática un objeto con una duración de almacenamiento estática en el ámbito del espacio de nombres (3.6.2). De lo contrario, tal objeto se inicializa la primera vez que el control pasa a través de su declaración; un objeto de este tipo se considera inicializado una vez finalizada su inicialización.
Suponiendo que la segunda parte se mantenga (el object is initialized the first time control passes through its declaration
), su código puede considerarse seguro para subprocesos.
Leyendo 3.6.2, parece que la inicialización temprana permitida está convirtiendo la inicialización dinámica en inicialización estática . Dado que la inicialización estática debe ocurrir antes de cualquier inicialización dinámica y como no puedo pensar en ninguna forma de crear un subproceso hasta que llegue a la inicialización dinámica , tal inicialización temprana también garantizaría que el constructor sea llamado una sola vez.
Actualizar
Entonces, con respecto a llamar al constructor the_const_thingy
para the_const_thingy
, su código es correcto de acuerdo con las reglas.
Esto deja el problema de sobrescribir la referencia que definitivamente no está cubierta por la especificación. Dicho esto, si está dispuesto a asumir que las referencias se implementan a través de punteros (lo que creo que es la forma más común de hacerlo), todo lo que va a hacer es sobrescribir un puntero con el valor que ya tiene. Así que mi opinión es que esto debería ser seguro en la práctica.
Este es mi segundo intento de respuesta. Solo contestaré la primera de tus preguntas:
- ¿Lo suficientemente seguro en la práctica?
No. Cuando se está declarando, solo se asegura de que la creación del objeto esté protegida, no la inicialización de la referencia al objeto.
En ausencia de un modelo de memoria C ++ 98 y ninguna declaración explícita del proveedor del compilador, no hay garantías de que la escritura en la memoria que representa la referencia real y la escritura en la memoria que contiene el valor del indicador de inicialización (si es eso) cómo se implementa) para la referencia se ven en el mismo orden de varios subprocesos.
Como también dice, sobrescribir la referencia varias veces con el mismo valor no debería hacer ninguna diferencia semántica (incluso en presencia de palabra tearing, que en general es poco probable y quizás incluso imposible en la arquitectura de su procesador), pero hay un caso en el que importa: cuándo Más de un hilo compite por primera vez durante la ejecución del programa . En este caso, es posible que uno o más de estos subprocesos vean cómo se establece el indicador de inicialización antes de que se inicialice la referencia real.
Tienes un error latente en tu programa y necesitas arreglarlo. En cuanto a las optimizaciones, estoy seguro de que hay muchas más además de usar el patrón de bloqueo de doble comprobación.
Este parece ser el enfoque más fácil / limpio que se me ocurre sin necesidad de usar todos los shananigans mutex:
static My_object My_object_instance()
{
static My_object object;
return object;
}
// Ensures that the instance is created before main starts and creates any threads
// thereby guaranteeing serialization of static instance creation.
__attribute__((constructor))
void construct_my_object()
{
My_object_instance();
}
He programado suficientes tomas interprocess para tener pesadillas. Para hacer que todo sea seguro para subprocesos en una CPU con RAM DDR, debe alinear en cache la línea de la estructura de datos y empaquetar todas sus variables globales de forma contigua en la menor cantidad posible de líneas de caché.
El problema con los datos entre procesos no alineados y las variables globales poco empaquetados es que causa fallas en el alias de caché. En las CPU que usan memoria RAM DDR, hay un (generalmente) un grupo de líneas de caché de 64 bytes. Cuando carga una línea de caché, la memoria RAM DDR cargará automáticamente un montón más de líneas de caché, pero la primera línea de caché siempre es la más caliente. Lo que sucede con las interrupciones que ocurren a altas velocidades es que la página de caché actuará como un filtro de paso bajo, como en las señales analógicas, y filtrará los datos de interrupción que conducen a errores COMPLETAMENTE desconcertantes si no está al tanto de lo que sucede. en. Lo mismo ocurre con las variables globales que no están empaquetadas en forma estricta; si ocupa varias líneas de memoria caché, se desincronizará a menos que tome una instantánea de las variables críticas del proceso y las pase a la pila y los registros para asegurarse de que los datos se sincronizan correctamente.
La sección .bss (es decir, donde se almacenan las variables globales, se inicializará a todos los ceros, pero el compilador no alineará los datos con la caché, tendrá que hacerlo usted mismo, lo que también puede ser un buen lugar para use C ++ Construct in Place . Para aprender las matemáticas detrás de la manera más rápida de alinear los punteros, lea este artículo ; estoy tratando de averiguar si se me ocurrió ese truco. A continuación se muestra el código:
inline char* AlignCacheLine (char* buffer) {
uintptr_t offset = ((~reinterpret_cast<uintptr_t> (buffer)) + 1) & (63);
return buffer + offset;
}
char SomeTypeInit (char* buffer, int param_1, int param_2, int param_3) {
SomeType type = SomeType<AlignCacheLine (buffer)> (1, 2, 3);
return 0xff;
}
const SomeType* create_const_thingy () {
static char interprocess_socket[sizeof (SomeType) + 63],
dead_byte = SomeTypeInit (interprocess_socket, 1, 2, 3);
return reinterpret_cast<SomeType*> (AlignCacheLine (interprocess_socket));
}
En mi experiencia, tendrá que usar un puntero, no una referencia.
No soy standardista ...
Pero para el uso que mencionas, ¿por qué no simplemente inicializarlos antes de crear cualquier hilo? Muchos problemas de Singleton se deben a que las personas utilizan la inicialización perezosa idiomática de "subproceso único", mientras que simplemente podrían instanciar el valor cuando se carga la biblioteca (como un típico global).
La moda perezosa solo tiene sentido si usa este valor de otro ''global''.
Por otro lado, otro método que he visto fue usar algún tipo de coordinación:
- ''Singleton'' para registrar su método de inicialización en un objeto ''GlobalInitializer'' durante el tiempo de carga de la biblioteca
- ''GlobalInitializer'' se llamará en ''main'' antes de que se inicie cualquier subproceso
aunque no lo esté describiendo con precisión.
Simplemente llame a la función antes de comenzar a crear subprocesos, garantizando así la referencia y el objeto. Alternativamente, no uses un patrón de diseño realmente terrible. Quiero decir, ¿por qué en la tierra tienen una referencia estática a un objeto estático? ¿Por qué incluso tener objetos estáticos? No hay beneficio para esto. Singletons es una idea terrible.