Escuché que eso constsignifica 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 constobjetos sean seguras para subprocesos . Esto significa que la biblioteca estándar no introducirá una carrera de datos siempre que las operaciones en constobjetos 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, constsignifica 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 constestá presente o no, no afectará la generación de código, al menos no con respecto a las carreras de datos .
Significa eso constestá 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 areadesde tantos hilos como desee y obtendrá resultados correctos todo el tiempo.
Tenga en cuenta que esto no significa que rectsea seguro para subprocesos . De hecho, es fácil ver cómo si areaocurriera una llamada a al mismo tiempo que una llamada a set_sizeen un determinado rect, entonces areapodría terminar calculando su resultado basado en un ancho anterior y una nueva altura (o incluso en valores confusos) .
Pero eso está bien, rectno es constasí 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, constentonces 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 intpor 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 areapuede 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 recten un contenedor estándar, directa o indirectamente, estamos firmando un contrato con la Biblioteca estándar . Para seguir haciendo escrituras en una constfunció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 areafunción segura para subprocesos , pero recttodavía no es segura para subprocesos . Un llamado a areasuceder al mismo tiempo que una llamada a set_sizetodavía puede llegar a calcular el valor erróneo, ya que las asignaciones a widthy heightno 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 constymutable - Herb Sutter