Еще одна проблема при разрушении статиков

#опытным



Идею для поста подкинул Михаил в этом комменте



Суть в чем. Все глобальные переменные, не помеченные thread_local, создаются и уничтожаются в главном потоке, в котором выполняется main(). Но использовать мы их можем и в других потоках, адресное пространство-то одно. И вот здесь скрывается опасность: мы можем использовать в другом потоке глобальную переменную, которая уже была уничтожена!



Вы просите объяснений? Их есть у меня.



Для начала нужно понять, при каких условиях мы можем получить ситуацию, при которой статическая переменная уже удалилась, программа еще не завершилась, а другой тред продолжает использовать переменную.



По пунктам



1️⃣ Статические переменные удаляются при вызове std::exit, что происходит после завершения main(). Значит, нам нужно выйти из main'а.



2️⃣ Получается, что второй поток должен продолжать выполняться даже после завершения main. Тут только один вариант: отделить тред от его объекта, чтобы его не нужно было джойнить. Делается это с помощью метода detach().



3️⃣ Использование переменной вторым потоком должно быть между разрушением глобальной переменной и завершением std::exit, потому что эта функция завершает процесс. И естественно, что после завершения процесса уже никакие потоки выполняться не могут.



Вот такие незамысловатые условия. Давайте посмотрим на примере.





struct A {

~A() {

std::this_thread::sleep_for(std::chrono::seconds(5));

}

};



struct B {

std::string str = "Use me";

~B() {

std::cout << "B dtor" << std::endl;;

}

};



A global_for_waiting_inside_globals_dectruction;

B violated_global;



void Func() {

for (int i = 0; i < 20; ++i) {

std::cout << violated_global.str << std::endl;

std::this_thread::sleep_for(std::chrono::seconds(1));

}

}



int main() {

std::thread th{Func};

th.detach();

std::this_thread::sleep_for(std::chrono::seconds(3)); // aka some usefull work

}





Быстренькое пояснение. Создал 2 простеньких класса, которые позволят наглядно показать процесс удаления переменной и использования ее после удаления. Деструктор первого класса заставляет главный тред уснуть на 5 секунд, что помещает программу в опасное состояние как раз между ее завершением и разрушением статиков. Второй класс мы как раз и будем использовать для создания шаренного объекта, который использует второй тред. У него в деструкторе выводится сообщение-индикатор удаления. Давайте посмотрим на вывод:



Use me

Use me

Use me

B dtor

Use me

Use me

Use me

Use me

Use me





Поймана за хвост, паршивка! Мы используем поле удаленного объекта, что чистой воды UB!



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



Если я что-то упустил, то пусть Михаил меня поправит в комментах.



Avoid dangerous practices. Stay cool.



#cppcore #cpp11 #concurrency