¿Mi compilador ignoró mi miembro de clase static thread_local no utilizado?

10

Quiero hacer un registro de hilo en mi clase, así que decido agregar un cheque para la thread_localfunción:

#include <iostream>
#include <thread>

class Foo {
 public:
  Foo() {
    std::cout << "Foo()" << std::endl;
  }
  ~Foo() {
    std::cout << "~Foo()" << std::endl;
  }
};

class Bar {
 public:
  Bar() {
    std::cout << "Bar()" << std::endl;
    //foo;
  }
  ~Bar() {
    std::cout << "~Bar()" << std::endl;
  }
 private:
  static thread_local Foo foo;
};

thread_local Foo Bar::foo;

void worker() {
  {
    std::cout << "enter block" << std::endl;
    Bar bar1;
    Bar bar2;
    std::cout << "exit block" << std::endl;
  }
}

int main() {
  std::thread t1(worker);
  std::thread t2(worker);
  t1.join();
  t2.join();
  std::cout << "thread died" << std::endl;
}

El código es simple. Mi Barclase tiene un thread_localmiembro estático foo. Si thread_local Foo foose crea una estática , significa que se crea un hilo.

Pero cuando ejecuto el código, no Foo()aparece nada en las impresiones, y si elimino el comentario en Barel constructor, que utiliza foo, el código funciona bien.

Probé esto en GCC (7.4.0) y Clang (6.0.0) y los resultados son los mismos. Supongo que el compilador descubrió que foono está en uso y no genera una instancia. Entonces

  1. ¿El compilador ignoró al static thread_localmiembro? ¿Cómo puedo depurar esto?
  2. Si es así, ¿por qué un staticmiembro normal no tiene este problema?
ravenisadesk
fuente

Respuestas:

9

No hay problema con tu observación. [basic.stc.static] / 2 prohíbe eliminar variables con duración de almacenamiento estático:

Si una variable con duración de almacenamiento estático tiene inicialización o un destructor con efectos secundarios, no se eliminará incluso si parece no estar usado, excepto que un objeto de clase o su copia / movimiento puede eliminarse como se especifica en [class.copy] .

Esta restricción no está presente para otras duraciones de almacenamiento. De hecho, [basic.stc.thread] / 2 dice:

Una variable con duración de almacenamiento de subprocesos se inicializará antes de su primer uso odr y, si se construye , se destruirá al salir del subproceso.

Esto sugiere que no es necesario construir una variable con duración de almacenamiento de subprocesos a menos que se use odr.


Pero, ¿por qué es esta discrepancia?

Para la duración del almacenamiento estático, solo hay una instancia de una variable por programa. Los efectos secundarios de la construcción de los mismos pueden ser significativos (como un constructor de todo el programa), por lo que los efectos secundarios son obligatorios.

Sin embargo, para la duración del almacenamiento local de subprocesos, hay un problema: un algoritmo puede iniciar muchos subprocesos. Para la mayoría de estos hilos, la variable es completamente irrelevante. Sería divertido si una biblioteca de simulación física externa que llama std::reduce(std::execution::par_unseq, first, last)termina creando muchas fooinstancias, ¿verdad?

Por supuesto, puede haber un uso legítimo para los efectos secundarios de la construcción de variables de duración de almacenamiento local de subprocesos que no se utilizan odr (por ejemplo, un rastreador de subprocesos). Sin embargo, la ventaja de garantizar esto no es suficiente para compensar el inconveniente mencionado anteriormente, por lo que estas variables se pueden eliminar siempre que no se utilicen de forma habitual. (Sin embargo, su compilador puede elegir no hacerlo. Y también puede hacer su propio envoltorio std::threadque se encargue de esto).

LF
fuente
1
De acuerdo ... si el estándar lo dice, entonces que así sea ... Pero ¿no es extraño que el comité no considere los efectos secundarios como la duración del almacenamiento estático?
ravenisadesk el
@reavenisadesk Ver respuesta actualizada.
LF
1

Encontré esta información en " Manejo de ELF para almacenamiento local de subprocesos " que puede probar la respuesta de @LF

Además, el soporte en tiempo de ejecución debe evitar crear el almacenamiento local de subprocesos si no es necesario. Por ejemplo, un módulo cargado solo puede ser utilizado por un hilo de los muchos que componen el proceso. Sería una pérdida de memoria y tiempo asignar el almacenamiento para todos los hilos. Se busca un método perezoso. Esto no es una carga adicional, ya que el requisito de manejar objetos cargados dinámicamente ya requiere reconocer el almacenamiento que aún no está asignado. Esta es la única alternativa para detener todos los subprocesos y asignar almacenamiento para todos los subprocesos antes de permitir que se ejecuten nuevamente.

ravenisadesk
fuente