__cxa_thread_atexit
Вот если посмотреть пристально на обычную сосиску. Длинная, похожа на палку. А что говорят древние философы?
Палка о двух концах! А это о чем говорит? О том, что концы-то у неё есть, а начала — нет! Значит, по науке, сосиска есть предмет безначальный!
О чем это говорит? Сосиска - это революция. Потому как есть у революции начало, нет у революции конца!
Что же касается потоков, мы можем точно сказать, что они не революция. Конец у них есть, и мы можем легко его обнаружить!
Стоит лишь внимательнее присмотреться к thread_local, ведь такие объекты будут удалять самыми последними.
Обычный объект в потоке - дело скучное:
Не случится ничего интересного, просто в конце потока вызовется деструктор объекта.
Теперь чуть разнообразия в томный вечер:
Жизнь заиграла новыми красками.
Во-первых, пропадет вызов деструктора Signal.
Во-вторых, при создании объекта произойдет следующая удивительная метаморфоза:
Что же за непотребство здесь творится?!
Да это просто компилятор регистрирует деструктор thread_local объекта через функцию __cxa_thread_atexit.
Это специальная внутренняя плюсовая функция, в которую можно вписать функцию очистки, и она гарантированно будет вызвана при завершении работы потока.
Для деструкторов работает особая уличная магия компилятора, но почему бы не пошалить и не засунуть туда какой-нибудь свой обработчик? Кто нам запретит?
Например, желаю в самом конце потока удалить кое-какие чувствительные данные, ну и отладочную печать для наглядности тиснуть.
В качестве аргумента получаем указатель. Допустим, мы знаем, что это некий int. В функции приведем его в надлежащий вид и удалим.
Теперь вручную зарегистрируем наш уничтожитель. Для этого нам потребуется несколько магических пассов. Призовем в код таинственную функцию __cxa_thread_atexit и служебную переменную для линкера:
Соответственно, на экране мы увидим
Ой, наш чистильщик пришел слишком рано, перед деструктором Signal.
Все дело в порядке вызовов __cxa_thread_atexit: Поскольку объект Signal создается первый, то и деструктор он регистрирует первым. А в конце потока специально обученная функция вызывает все втиснутые в список функции в обратном порядке.
Если вызвать регистратор перед созданием Signal,
то все будет выглядеть иначе:
Теперь порядок, my_thread_cleanup выполнится в самом конце потока. А поток, как известно, не сосиска.
Вот если посмотреть пристально на обычную сосиску. Длинная, похожа на палку. А что говорят древние философы?
Палка о двух концах! А это о чем говорит? О том, что концы-то у неё есть, а начала — нет! Значит, по науке, сосиска есть предмет безначальный!
О чем это говорит? Сосиска - это революция. Потому как есть у революции начало, нет у революции конца!
Что же касается потоков, мы можем точно сказать, что они не революция. Конец у них есть, и мы можем легко его обнаружить!
Стоит лишь внимательнее присмотреться к thread_local, ведь такие объекты будут удалять самыми последними.
Обычный объект в потоке - дело скучное:
void thread_worker(int id) {
Signal s{1};
...
Не случится ничего интересного, просто в конце потока вызовется деструктор объекта.
bl Signal::~Signal() [complete object destructor]
Теперь чуть разнообразия в томный вечер:
void thread_worker(int id) {
thread_local Signal tls{2};
...
Жизнь заиграла новыми красками.
Во-первых, пропадет вызов деструктора Signal.
Во-вторых, при создании объекта произойдет следующая удивительная метаморфоза:
movw r2, #:lower16:__dso_handle
movt r2, #:upper16:__dso_handle
mov r1, r3
movw r0, #:lower16:Signal::~Signal() [complete object destructor]
movt r0, #:upper16:Signal::~Signal() [complete object destructor]
bl __cxa_thread_atexit
Что же за непотребство здесь творится?!
Да это просто компилятор регистрирует деструктор thread_local объекта через функцию __cxa_thread_atexit.
Это специальная внутренняя плюсовая функция, в которую можно вписать функцию очистки, и она гарантированно будет вызвана при завершении работы потока.
Для деструкторов работает особая уличная магия компилятора, но почему бы не пошалить и не засунуть туда какой-нибудь свой обработчик? Кто нам запретит?
Например, желаю в самом конце потока удалить кое-какие чувствительные данные, ну и отладочную печать для наглядности тиснуть.
void my_thread_cleanup(void* arg) {
int* id = static_cast<int*>(arg);
std::cout << "[Cleanup] Поток " << *id << " завершился. Чистим память...\n";
delete id;
}
В качестве аргумента получаем указатель. Допустим, мы знаем, что это некий int. В функции приведем его в надлежащий вид и удалим.
Теперь вручную зарегистрируем наш уничтожитель. Для этого нам потребуется несколько магических пассов. Призовем в код таинственную функцию __cxa_thread_atexit и служебную переменную для линкера:
extern "C" int __cxa_thread_atexit(void (*func)(void *), void *obj, void *dso_handle);
extern void* __dso_handle;
void thread_worker(int id) {
thread_local Signal tls{2};
int* thread_data = new int(id);
__cxa_thread_atexit(my_thread_cleanup, thread_data, &__dso_handle);
std::cout << "[Thread] Поток " << id << " выполнил работу.\n";
}
Соответственно, на экране мы увидим
+sig2
[Thread] Поток 1 выполнил работу.
[Cleanup] Поток 1 завершился. Чистим память...
-sig2
Ой, наш чистильщик пришел слишком рано, перед деструктором Signal.
Все дело в порядке вызовов __cxa_thread_atexit: Поскольку объект Signal создается первый, то и деструктор он регистрирует первым. А в конце потока специально обученная функция вызывает все втиснутые в список функции в обратном порядке.
Если вызвать регистратор перед созданием Signal,
__cxa_thread_atexit(my_thread_cleanup, thread_data, &__dso_handle);
thread_local Signal tls{2};
то все будет выглядеть иначе:
+sig2
[Thread] Поток 1 выполнил работу.
-sig2
[Cleanup] Поток 1 завершился. Чистим память...
Теперь порядок, my_thread_cleanup выполнится в самом конце потока. А поток, как известно, не сосиска.