调用join后DLL单例的线程未在程序终止前正常结束是什么原因?
问题现象
开发中编写了包含单例类的动态链接库(DLL),单例类构造函数创建工作线程,析构函数调用join等待线程结束。主程序调用getInstance()获取实例时线程可正常创建运行,但程序终止触发单例析构,执行join后线程未完成执行,缺少MySingleton::runner end日志,与预期不符。
实际输出
MySingleton::runner start a MySingleton::~MySingleton begin. MySingleton::~MySingleton before calling join() MySingleton::~MySingleton after calling join() MySingleton::~MySingleton end.
预期输出
MySingleton::runner start a MySingleton::~MySingleton begin. MySingleton::~MySingleton before calling join() MySingleton::runner end. MySingleton::~MySingleton after calling join() MySingleton::~MySingleton end.
可正常输出的已知场景
- MySingleton::getInstance定义在头文件中
- 库编译为静态库(.lib)而非动态库(.dll)
- 主函数中直接定义MySingleton实例,不使用单例模式
补充说明:最初测试误以为用volatile bool running_{false}或std::atomic_bool running_{false}定义running_可解决问题,后续发现测试时误将库编译为静态库,实际在动态库场景下,使用std::atomic修饰running_、用mutex保护running_访问都无法解决该问题,析构函数打印的counter值可证明线程循环此前一直在正常执行。
相关代码
Singleton.h
class MySingleton { private: DllExport MySingleton(); DllExport ~MySingleton(); public: DllExport static MySingleton& getInstance(); MySingleton(MySingleton const&) = delete; void operator=(MySingleton const&) = delete; private: DllExport void runner(); std::thread th_; std::atomic_bool running_{false}; std::atomic<size_t> counter_{0}; };
Singleton.cpp
MySingleton::MySingleton() { running_ = true; th_ = std::thread(&MySingleton::runner, this); } MySingleton::~MySingleton() { std::cout << __FUNCTION__ << " begin." << std::endl; running_ = false; if (th_.joinable()) { std::cout << __FUNCTION__ << " before calling join()" << std::endl; th_.join(); std::cout << __FUNCTION__ << " after calling join()" << std::endl; } std::cout << "Count: " << counter_ << std::endl; std::cout << __FUNCTION__ << " end." << std::endl; } MySingleton &MySingleton::getInstance() { static MySingleton single; return single; } void MySingleton::runner() { std::cout << __FUNCTION__ << " start " << std::endl; while (running_) { counter_++; } std::cout << __FUNCTION__ << " end " << std::endl; }
main.cpp
int main() { MySingleton::getInstance(); std::string s; std::cin >> s; return 0; }
DllExport.h
#ifdef DLL_EXPORT #define DllExport __declspec(dllexport) #else #define DllExport __declspec(dllimport) #endif
CMakeLists.txt
cmake_minimum_required(VERSION 3.13) project("test") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_EXTENSIONS OFF) add_library(singleton SHARED Singleton.cpp) target_compile_definitions(singleton PUBLIC -DDLL_EXPORT) target_include_directories(singleton PUBLIC ./) install(TARGETS singleton EXPORT singleton-config CONFIGURATIONS ${CMAKE_BUILD_TYPE} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib ) install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/ DESTINATION bin FILES_MATCHING PATTERN "*.dll" PATTERN "*.pdb" ) add_executable(main main.cpp ) target_link_libraries(main PUBLIC singleton) install(TARGETS main RUNTIME DESTINATION bin)
原因解答
该问题是Windows平台MSVC环境下的DLL卸载逻辑与静态对象析构顺序冲突导致的:
- Windows程序退出时,先执行主程序所有静态/全局对象的析构,之后调用
ExitProcess,系统会按加载顺序的逆序卸载所有已加载的DLL。 - 你的单例是定义在DLL的
getInstance函数内的静态局部变量,属于DLL范围内的静态对象,它的析构是在DLL被卸载时才触发的。 - 当DLL进入卸载流程时,MSVC的C++运行时会先执行当前DLL内所有静态对象的析构,但此时DLL的代码段、数据段已经进入待回收状态,DLL中创建的非主线程会被系统强制终止,不会再继续执行后续代码。
- 你在单例析构里设置
running_ = false之后调用join,但是此时工作线程已经被系统提前杀掉,自然不会执行到runner里退出循环后的MySingleton::runner end打印,join也会直接返回。
其他场景正常的原因
- getInstance定义在头文件:此时静态局部单例的实例会被创建在主程序的运行时上下文里,析构时机是主程序退出时、DLL卸载之前,此时DLL的代码段还处于正常可用状态,线程可以正常执行完退出逻辑。
- 编译为静态库:静态库的代码会直接链接到主程序镜像中,单例属于主程序的静态对象,析构在主程序退出流程中完成,不会触发DLL卸载的提前杀线程逻辑。
- 主函数直接定义实例:实例属于主程序的栈对象,main函数返回前就会析构,远早于DLL卸载流程,线程自然可以正常执行完成。
解决方案
可任选一种方案规避问题:
- 给单例加一个主动销毁的静态方法,在main函数返回前显式调用,提前触发单例析构和线程join,不要等DLL卸载时自动析构。
- 将getInstance的实现移到头文件中,让单例实例创建在主程序的上下文里。
- 改用静态库编译,避免DLL卸载流程的影响。
内容的提问来源于stack exchange,提问作者Braiden Cutforth
相关产品推荐
相关产品推荐

