为什么在调用 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.

有些情况下线程如我所料结束(并且我得到了预期的输出):

  1. MySingleton::getInstance 在 header
  2. 中定义
  3. 库被编译为 .lib 而不是 .dll(静态库)
  4. MySingleton singleton 主定义body(非单例)

我无法弄清楚为什么只有在某些情况下线程没有按预期结束,我不明白是否与MSVC有关,静态成员函数中静态局部变量的销毁方式,或者它是否与我如何 create/join 线程或其他东西有关。

编辑

出现预期输出的更多情况:

  1. 定义volatile bool running_{false}(可能不是正确的解决方案)
  2. 定义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:

  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.

运行线程在 (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收割之前。