Home > Blockchain >  Why won't my thread end properly before my program terminates after calling join?
Why won't my thread end properly before my program terminates after calling join?

Time:09-29

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):

  1. MySingleton::getInstance is defined in the header
  2. The library is compiled as a .lib instead of .dll (Static lib)
  3. 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:

  1. Defining volatile bool running_{false} (may not be a proper solution)
  2. Defining std::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)

CodePudding user response:

The problem seems to be that the thread is terminated abruptly by ExitProcess (called implicitly when main returns IIRC).

Exiting a process causes the following:

  1. All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH notification.
  2. The states of all of the threads terminated in step 1 become signaled.
  3. The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
  4. After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the calling thread.
  5. The state of the calling thread becomes signaled.
  6. All of the object handles opened by the process are closed.
  7. The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
  8. The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.

The runner thread is terminated in (1) while the destructor is only called in (3).

CodePudding user response:

The accepted answer explains what happens when you put your threaded singleton in a DLL so the question has been answered.

Here's a suggestion how to circumvent this behavior.

Your 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>;

The user of the DLL would now use

    auto& instance = FancyName::getInstance();

and the destruction should occur before the thread is reaped by ExitProcess.

  • Related