I have created a shared library (DLL) which contains a singleton class. The singleton class creates a thread on construction, and calls join in the destructor. See code below.
When I use the DLL in another program (main.cpp) and get the singleton instance, the thread is created and runs as expected. When the program terminates, the singleton destructor is called, the thread join is called but the thread does not complete.
The output I get from the program:
MySingleton::runner start
a
MySingleton::~MySingleton begin.
MySingleton::~MySingleton before calling join()
MySingleton::~MySingleton after calling join()
MySingleton::~MySingleton end.
Expected Output:
MySingleton::runner start
a
MySingleton::~MySingleton begin.
MySingleton::~MySingleton before calling join()
MySingleton::runner end.
MySingleton::~MySingleton after calling join()
MySingleton::~MySingleton end.
There are some situations where the thread ends as I expect (and I get expected output):
- MySingleton::getInstance is defined in the header
- The library is compiled as a .lib instead of .dll (Static lib)
MySingleton singleton
definition in main body (not singleton)
I cannot figure out why the thread does not end as expected only in some cases, and I don't understand if it is something to do with MSVC, the way static local variables in a static member function are destructed, or if it is something to do with how I create/join the thread or something else altogether.
EDIT
More situations where expected output happens:
Definingvolatile bool running_{false}
(may not be a proper solution)Definingstd::atomic_bool running_{false}
Seems to be the proper way, or using a mutex.
EDIT 2
Using std::atomic for the running_
variable did not work (although code adjusted below to use it as we don't want UB). I accidentally built as a static library when I was testing std::atomic and volatile and as previously mentioned the issue does not occur with a static library.
I have also tried protecting running_
with a mutex and still have the weird behaviour. (I acquire a lock in an while(true)
loop, check !running_
to break
.)
I have also updated the thread loop below to increment a counter, and the destructor will print this value (showing loop is actually executing).
// 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)