Escuché que eso const
significa seguro para subprocesos en C ++ 11 . ¿Es eso cierto?
Es algo cierto ...
Esto es lo que tiene que decir el lenguaje estándar sobre seguridad de subprocesos:
[1.10 / 4]
Dos evaluaciones de expresión entran en conflicto si una de ellas modifica una ubicación de memoria (1.7) y la otra accede o modifica la misma ubicación de memoria.
[1.10 / 21]
La ejecución de un programa contiene una carrera de datos si contiene dos acciones en conflicto en diferentes subprocesos, al menos una de las cuales no es atómica, y ninguna ocurre antes que la otra. Cualquier carrera de datos de este tipo da como resultado un comportamiento indefinido.
que no es más que la condición suficiente para que se produzca una carrera de datos :
- Se están realizando dos o más acciones al mismo tiempo en una cosa determinada; y
- Al menos uno de ellos es escrito.
La biblioteca estándar se basa en eso, yendo un poco más allá:
[17.6.5.9/1]
Esta sección especifica los requisitos que deben cumplir las implementaciones para evitar carreras de datos (1.10). Cada función de biblioteca estándar debe cumplir con cada requisito a menos que se especifique lo contrario. Las implementaciones pueden evitar carreras de datos en casos distintos a los que se especifican a continuación.
[17.6.5.9/3]
Una función de biblioteca estándar de C ++ no modificará directa o indirectamente objetos (1.10) accesibles por subprocesos distintos del subproceso actual a menos que se acceda a los objetos directa o indirectamente a través de losargumentosno constantes de la función, incluidosthis
.
que en palabras simples dice que espera que las operaciones en const
objetos sean seguras para subprocesos . Esto significa que la biblioteca estándar no introducirá una carrera de datos siempre que las operaciones en const
objetos de su propio tipo tampoco
- Consiste íntegramente en lecturas --es decir, no hay escrituras--; o
- Sincroniza internamente escrituras.
Si esta expectativa no es válida para uno de sus tipos, usarlo directa o indirectamente junto con cualquier componente de la Biblioteca estándar puede resultar en una carrera de datos . En conclusión, const
significa seguro para subprocesos desde el punto de vista de la biblioteca estándar . Es importante tener en cuenta que esto es simplemente un contrato y el compilador no lo hará cumplir, si lo rompe, obtendrá un comportamiento indefinido y estará solo. Si const
está presente o no, no afectará la generación de código, al menos no con respecto a las carreras de datos .
Significa eso const
está ahora el equivalente de Java s' synchronized
?
No se . De ningún modo...
Considere la siguiente clase demasiado simplificada que representa un rectángulo:
class rect {
int width = 0, height = 0;
public:
/*...*/
void set_size( int new_width, int new_height ) {
width = new_width;
height = new_height;
}
int area() const {
return width * height;
}
};
La función miembro area
es segura para subprocesos ; no porque sea const
, sino porque consiste enteramente en operaciones de lectura. No hay escrituras involucradas, y al menos una escritura involucrada es necesaria para que ocurra una carrera de datos . Eso significa que puede llamar area
desde tantos hilos como desee y obtendrá resultados correctos todo el tiempo.
Tenga en cuenta que esto no significa que rect
sea seguro para subprocesos . De hecho, es fácil ver cómo si area
ocurriera una llamada a al mismo tiempo que una llamada a set_size
en un determinado rect
, entonces area
podría terminar calculando su resultado basado en un ancho anterior y una nueva altura (o incluso en valores confusos) .
Pero eso está bien, rect
no es const
así que ni siquiera se espera que sea seguro para subprocesos después de todo. Un objeto declarado const rect
, por otro lado, sería seguro para subprocesos ya que no es posible escribir (y si está considerando const_cast
-ing algo originalmente declarado, const
entonces obtiene un comportamiento indefinido y eso es todo).
Entonces, ¿qué significa eso?
Supongamos, por el bien del argumento, que las operaciones de multiplicación son extremadamente costosas y es mejor evitarlas cuando sea posible. Podríamos calcular el área solo si se solicita, y luego almacenarla en caché en caso de que se solicite nuevamente en el futuro:
class rect {
int width = 0, height = 0;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
cached_area_valid = ( width == new_width && height == new_height );
width = new_width;
height = new_height;
}
int area() const {
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
[Si este ejemplo parece demasiado artificial, podría reemplazarlo mentalmente int
por un entero muy grande asignado dinámicamente que no es inherentemente seguro para subprocesos y para el cual las multiplicaciones son extremadamente costosas].
La función miembro area
ya no es segura para subprocesos , está escribiendo ahora y no está sincronizada internamente. ¿Es un problema? La llamada a area
puede ocurrir como parte de un constructor de copia de otro objeto, tal constructor podría haber sido llamado por alguna operación en un contenedor estándar , y en ese punto la biblioteca estándar espera que esta operación se comporte como una lectura con respecto a las carreras de datos. . ¡Pero estamos escribiendo!
Tan pronto como colocamos un rect
en un contenedor estándar, directa o indirectamente, estamos firmando un contrato con la Biblioteca estándar . Para seguir haciendo escrituras en una const
función sin dejar de cumplir con ese contrato, necesitamos sincronizar internamente esas escrituras:
class rect {
int width = 0, height = 0;
mutable std::mutex cache_mutex;
mutable int cached_area = 0;
mutable bool cached_area_valid = true;
public:
/*...*/
void set_size( int new_width, int new_height ) {
if( new_width != width || new_height != height )
{
std::lock_guard< std::mutex > guard( cache_mutex );
cached_area_valid = false;
}
width = new_width;
height = new_height;
}
int area() const {
std::lock_guard< std::mutex > guard( cache_mutex );
if( !cached_area_valid ) {
cached_area = width;
cached_area *= height;
cached_area_valid = true;
}
return cached_area;
}
};
Tenga en cuenta que hicimos la area
función segura para subprocesos , pero rect
todavía no es segura para subprocesos . Un llamado a area
suceder al mismo tiempo que una llamada a set_size
todavía puede llegar a calcular el valor erróneo, ya que las asignaciones a width
y height
no están protegidos por el mutex.
Si realmente quisiéramos un hilo seguro rect
, usaríamos una primitiva de sincronización para proteger el no seguro para hilos rect
.
¿Se están quedando sin palabras clave ?
Sí lo son. Se han estado quedando sin palabras clave desde el primer día.
Fuente : No lo sabes const
ymutable
- Herb Sutter