Мой компилятор проигнорировал мой неиспользуемый статический член класса thread_local?

Я хочу выполнить регистрацию потока в своем классе, поэтому я решил добавить проверку для функции thread_local:

#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;
}

Код прост. В моем классе Bar есть статический член thread_local foo. Если создается статический thread_local Foo foo, это означает, что создается поток.

Но когда я запускаю код, в Foo() ничего не печатается, и если я удалю комментарий в конструкторе Bar, который использует foo, код работает нормально.

Я попробовал это на GCC (7.4.0) и Clang (6.0.0), и результаты такие же. Я предполагаю, что компилятор обнаружил, что foo не используется, и не генерирует экземпляр. Так

  1. Компилятор проигнорировал элемент static thread_local? Как я могу отладить это?
  2. Если да, то почему у обычного члена static нет этой проблемы?

person Community    schedule 04.11.2019    source источник


Ответы (2)


С вашим наблюдением проблем нет. [basic.stc.static]/2 запрещает удаление переменные со статической продолжительностью хранения:

Если переменная со статической продолжительностью хранения имеет инициализацию или деструктор с побочными эффектами, она не должна быть устранена, даже если кажется, что она не используется, за исключением того, что объект класса или его копия/перемещение могут быть устранены, как указано в [class.copy] .

Это ограничение не действует для других сроков хранения. Фактически, [basic.stc.thread]/2 говорит:

Переменная с продолжительностью хранения потока должна быть инициализирована до ее первого использования odr и, если создана, должна быть уничтожена при выходе из потока.

Это говорит о том, что переменную с продолжительностью хранения потока не нужно создавать, если не используется odr.


Но почему такое несоответствие?

Для продолжительности статического хранения существует только один экземпляр переменной для каждой программы. Побочные эффекты его построения могут быть значительными (вроде общепрограммного конструктора), поэтому побочные эффекты необходимы.

Однако для длительности локального хранения потока существует проблема: алгоритм может запускать много потоков. Для большинства этих потоков переменная совершенно не имеет значения. Было бы забавно, если бы внешняя библиотека физического моделирования, которая вызывает std::reduce(std::execution::par_unseq, first, last), в конечном итоге создала множество экземпляров foo, верно?

Конечно, можно законно использовать побочные эффекты построения переменных длительности локального хранения потока, которые не используются odr (например, средство отслеживания потоков). Однако преимущества гарантии этого недостаточно, чтобы компенсировать вышеупомянутый недостаток, поэтому эти переменные можно исключить, если они не используются odr. (Однако ваш компилятор может этого не делать. И вы также можете создать свою собственную оболочку вокруг std::thread, которая позаботится об этом.)

person L. F.    schedule 04.11.2019
comment
Хорошо... если так сказано в стандарте, то так тому и быть... Но не странно ли, что комитет не считает побочные эффекты продолжительностью статического хранения? - person ; 04.11.2019

Я нашел эту информацию в "Обработка ELF для локального хранилища потока", которая может доказать @ Л.Ф. ответ

Кроме того, поддержка времени выполнения должна избегать создания локального хранилища потока, если в этом нет необходимости. Например, загруженный модуль может использоваться только одним потоком из многих, составляющих процесс. Было бы пустой тратой памяти и времени выделять хранилище для всех потоков. Нужен ленивый метод. Это не является большой дополнительной нагрузкой, поскольку требование обработки динамически загружаемых объектов уже требует распознавания памяти, которая еще не выделена. Это единственная альтернатива остановке всех потоков и выделению памяти для всех потоков перед их повторным выполнением.

person Community    schedule 10.01.2020