为什么在调用 join 后我的程序终止之前我的线程不能正常结束?
Why won't my thread end properly before my program terminates after calling join?
我创建了一个包含单例的共享库 (DLL) class。单例 class 在构造时创建一个线程,并在析构函数中调用 join。请参阅下面的代码。
当我在另一个程序 (main.cpp) 中使用 DLL 并获取单例实例时,线程被创建并按预期运行。当程序终止时,调用单例析构函数,调用线程连接但线程没有完成。
我从程序中得到的输出:
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 在 header
中定义
- 库被编译为 .lib 而不是 .dll(静态库)
MySingleton singleton
主定义body(非单例)
我无法弄清楚为什么只有在某些情况下线程没有按预期结束,我不明白是否与MSVC有关,静态成员函数中静态局部变量的销毁方式,或者它是否与我如何 create/join 线程或其他东西有关。
编辑
出现预期输出的更多情况:
定义volatile bool running_{false}
(可能不是正确的解决方案)
定义std::atomic_bool running_{false}
似乎是正确的方法,或者使用互斥锁。
编辑 2
对 running_
变量使用 std::atomic 无效(尽管在下面调整代码以使用它,因为我们不想要 UB)。我在测试 std::atomic 和 volatile 时不小心将其构建为静态库,如前所述,静态库不会出现此问题。
我也试过用互斥量保护 running_
,但仍然有奇怪的行为。 (我在 while(true)
循环中获取了一个锁,检查 !running_
到 break
。)
我还更新了下面的线程循环以增加一个计数器,析构函数将打印这个值(显示循环正在实际执行)。
// 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
cmake_minimum_required(VERSION 3.13)
project("test")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTE NSIONS 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)
问题似乎是线程被 ExitProcess 突然终止(在 main
returns IIRC 时隐式调用)。
Exiting a process causes the following:
- All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH
notification.
- The states of all of the threads terminated in step 1 become signaled.
- The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
- After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the
calling thread.
- The state of the calling thread becomes signaled.
- All of the object handles opened by the process are closed.
- The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
- The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.
运行线程在 (1) 中终止,而析构函数仅在 (3) 中调用。
已接受的答案解释了当您将线程单例放入 DLL 时会发生什么,因此问题已得到解答。
这里有一个如何避免这种行为的建议。
你的DLL.hpp
// A wrapper that is bound to be compiled into the users binary, not into the DLL:
template<class Singleton>
class InstanceMaker {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
class MySingleton {
private:
DllExport MySingleton();
DllExport ~MySingleton();
public:
// no getInstance() in here
friend class InstanceMaker<MySingleton>; // made a friend
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};
};
using FancyName = InstanceMaker<MySingleton>;
DLL 的用户现在可以使用
auto& instance = FancyName::getInstance();
并且销毁应该发生在线程被ExitProcess
收割之前。
我创建了一个包含单例的共享库 (DLL) class。单例 class 在构造时创建一个线程,并在析构函数中调用 join。请参阅下面的代码。
当我在另一个程序 (main.cpp) 中使用 DLL 并获取单例实例时,线程被创建并按预期运行。当程序终止时,调用单例析构函数,调用线程连接但线程没有完成。
我从程序中得到的输出:
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 在 header 中定义
- 库被编译为 .lib 而不是 .dll(静态库)
MySingleton singleton
主定义body(非单例)
我无法弄清楚为什么只有在某些情况下线程没有按预期结束,我不明白是否与MSVC有关,静态成员函数中静态局部变量的销毁方式,或者它是否与我如何 create/join 线程或其他东西有关。
编辑
出现预期输出的更多情况:
定义volatile bool running_{false}
(可能不是正确的解决方案)定义std::atomic_bool running_{false}
似乎是正确的方法,或者使用互斥锁。
编辑 2
对 running_
变量使用 std::atomic 无效(尽管在下面调整代码以使用它,因为我们不想要 UB)。我在测试 std::atomic 和 volatile 时不小心将其构建为静态库,如前所述,静态库不会出现此问题。
我也试过用互斥量保护 running_
,但仍然有奇怪的行为。 (我在 while(true)
循环中获取了一个锁,检查 !running_
到 break
。)
我还更新了下面的线程循环以增加一个计数器,析构函数将打印这个值(显示循环正在实际执行)。
// 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
cmake_minimum_required(VERSION 3.13)
project("test")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTE NSIONS 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)
问题似乎是线程被 ExitProcess 突然终止(在 main
returns IIRC 时隐式调用)。
Exiting a process causes the following:
- All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH notification.
- The states of all of the threads terminated in step 1 become signaled.
- The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
- After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the calling thread.
- The state of the calling thread becomes signaled.
- All of the object handles opened by the process are closed.
- The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
- The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.
运行线程在 (1) 中终止,而析构函数仅在 (3) 中调用。
已接受的答案解释了当您将线程单例放入 DLL 时会发生什么,因此问题已得到解答。
这里有一个如何避免这种行为的建议。
你的DLL.hpp
// A wrapper that is bound to be compiled into the users binary, not into the DLL:
template<class Singleton>
class InstanceMaker {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
class MySingleton {
private:
DllExport MySingleton();
DllExport ~MySingleton();
public:
// no getInstance() in here
friend class InstanceMaker<MySingleton>; // made a friend
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};
};
using FancyName = InstanceMaker<MySingleton>;
DLL 的用户现在可以使用
auto& instance = FancyName::getInstance();
并且销毁应该发生在线程被ExitProcess
收割之前。