靜態(tài)區(qū)析構(gòu)時引發(fā)的線程安全
背景
給openssl 1.0.2 是非線程安全的,需要CRYPTO_set_locking_callback設(shè)置函數(shù)來控制加鎖和解鎖.
example.cpp
std::vector<std::mutex> g_openssl_locks{static_cast<size_t>(CRYPTO_num_locks())};
//可能是多個線程在調(diào)用這個函數(shù)
void openssl_locking_function(int mode, int n, const char * /* file */, int /* line */) {
if(mode & CRYPTO_LOCK) {
g_openssl_locks[n].lock();
}
else {
g_openssl_locks[n].unlock();
}
}
CRYPTO_set_locking_callback(openssl_locking_function);
如果此時程序強行退出可能出現(xiàn)線程安全錯誤.
比如用instrument ThreadSanitizer運行測試的話就會報錯heap-use-after-free
原因
當(dāng)程序退出的時候,會銷毀全局/靜態(tài)對象. 此時別的線程可能還沒有終止,最后訪問了一個已經(jīng)被析構(gòu)的對象從而引發(fā)未知的問題.
調(diào)用 exit() 函數(shù)時阎肝,程序的終止流程通常遵循以下步驟:
- 調(diào)用 exit() 函數(shù):這可以發(fā)生在程序的任何地方放可,不限于主線程。
- 執(zhí)行 exit() 的初始操作:開始終止程序觉义,但不立即關(guān)閉所有線程。
- 銷毀靜態(tài)存儲期對象:全局對象和靜態(tài)局部對象會被銷毀,調(diào)用它們的析構(gòu)函數(shù)棍郎。
- 調(diào)用 atexit() 注冊的函數(shù):如果有通過 atexit() 注冊的函數(shù),這些函數(shù)會按照注冊的逆序被調(diào)用银室。
- 其他線程的強制終止:程序中的其他線程將被強制性地終止涂佃。這些線程不會正常完成它們的執(zhí)行路徑。
- 清理和關(guān)閉:進(jìn)行最終的清理操作蜈敢,包括關(guān)閉所有打開的文件和釋放其他系統(tǒng)資源辜荠。
- 程序終止:最后,控制權(quán)返回操作系統(tǒng)抓狭,程序完全結(jié)束伯病。
在這個過程中,并沒有為線程提供一個完整的否过、有序的終止機制午笛。這是因為 exit() 的設(shè)計是為了迅速終止程序,而不是等待或協(xié)調(diào)線程的安全退出苗桂。因此药磺,當(dāng)使用多線程時,如果需要優(yōu)雅地關(guān)閉線程煤伟,通常建議使用其他同步機制來確保線程能夠安全地完成它們的工作癌佩,而不是依賴 exit() 來結(jié)束程序木缝。
- C++標(biāo)準(zhǔn)參考: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2660.htm
The primary problem with destruction of static-duration objects is access to static-duration objects after their destructors have executed, thus resulting in undefined behavior. To prevent this problem, we require that all user threads finish before destruction begins. For threads that do not naturally finish, mechanisms to terminate threads are proposed in N2447 Multi-threading Library for Standard C++ and its initial incorporation in N2521 Working Draft, Standard for Programming Language C++.
這也是為什么C++標(biāo)準(zhǔn)中也提及了,需要在釋放全局/靜態(tài)變量之前,要保證所有線程都結(jié)束了.
編譯器自帶線程安全優(yōu)化
各編譯器也正對這一點有了優(yōu)化.
對于GCC >=4.3的版本已經(jīng)默認(rèn)屬性fthreadsafe-static
保證了靜態(tài)變量線程安全. 此屬性默認(rèn)是開啟的,大概就是能保證在線程都停止之后再析構(gòu). 有興趣可以繼續(xù)研究
參考鏈接:
https://gcc.gnu.org/onlinedocs/gcc/gcc-command-options/options-controlling-c%2B%2B-dialect.html#cmdoption-fthreadsafe-statics
各編譯器所支持的功能如下:
解決方案
如果你不能保證代碼會被什么編譯器和版本運行. 要兼容的話可以考慮這2個方案.
- 指定順序
遵循C++標(biāo)準(zhǔn). 如果你能控制子線程的話. 你只需要按照C++標(biāo)準(zhǔn)規(guī)定的順序即可. 先結(jié)束子線程再析構(gòu).
- 利用堆的特性. 通過將對象new出來放在堆區(qū). 堆區(qū)的生命周期是等待delete操作來釋放. 它不受exit()影響.
std::vector<std::mutex>* g_openssl_locks = new std::vector<std::mutex>(static_cast<size_t>(CRYPTO_num_locks()));
void openssl_locking_function(int mode, int n, const char * /* file */, int /* line */) {
if(mode & CRYPTO_LOCK) {
(*g_openssl_locks)[n].lock();
}
else {
(*g_openssl_locks)[n].unlock();
}
}
int g_sslinit = SetSslLocking();
此方式,沒人調(diào)用釋放g_openssl_locks. 它會等待程序完全結(jié)束后被操作系統(tǒng)回收. 保證了順序.
參考:
https://developer.aliyun.com/article/793257
https://zhuanlan.zhihu.com/p/656683028