¿Const significa seguro para subprocesos en C ++ 11?

115

Escuché que eso constsignifica seguro para subprocesos en C ++ 11 . ¿Es eso cierto?

Significa eso constestá ahora el equivalente de Java s' synchronized?

¿Se están quedando sin palabras clave ?

K-Ballo
fuente
1
C ++ - faq generalmente es administrado por la comunidad de C ++, y podría venir y pedirnos opiniones en nuestro chat.
Puppy
@DeadMG: No estaba al tanto de C ++ - faq y su etiqueta, se sugirió en un comentario.
K-Ballo
2
¿Dónde escuchaste que const significa seguro para subprocesos?
Mark B
2
@Mark B: Herb Sutter y Bjarne Stroustrup lo decían en Standard C ++ Foundation , vea el enlace al final de la respuesta.
K-Ballo
NOTA PARA LOS QUE VIENEN AQUÍ: la verdadera pregunta NO es si const significa seguro para subprocesos. Eso sería una tontería, ya que de lo contrario significaría que debería poder seguir adelante y marcar cada método seguro para subprocesos como const. Más bien, la pregunta que realmente estamos haciendo es const IMPLICA seguridad para subprocesos, y de eso se trata esta discusión.
user541686

Respuestas:

131

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 :

  1. Se están realizando dos o más acciones al mismo tiempo en una cosa determinada; y
  2. 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

  1. Consiste íntegramente en lecturas --es decir, no hay escrituras--; o
  2. 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

K-Ballo
fuente
6
@Ben Voigt: Tengo entendido que la especificación C ++ 11 para std::stringestá redactada de una manera que ya prohíbe COW . Sin embargo, no recuerdo los detalles ...
K-Ballo
3
@BenVoigt: No. Simplemente evitaría que tales cosas no estén sincronizadas, es decir, no sean seguras para subprocesos. C ++ 11 ya prohíbe explícitamente COW; sin embargo, este pasaje en particular no tiene nada que ver con eso y no prohibiría COW.
Puppy
2
Me parece que hay una brecha lógica. [17.6.5.9/3] prohíbe "demasiado" al decir "no modificará directa o indirectamente"; debería decir "no introducirá directa o indirectamente una carrera de datos", a menos que una escritura atómica esté definida en algún lugar para no ser una "modificación". Pero no puedo encontrar esto en ningún lado.
Andy Prowl
1
Probablemente dejé todo mi punto un poco más claro aquí: isocpp.org/blog/2012/12/… Gracias por intentar ayudar de todos modos.
Andy Prowl
1
a veces me pregunto quién fue el (o los directamente involucrados) realmente responsable de escribir algunos párrafos estándar como estos.
pepper_chico