线程在完成执行代码之前在动态库中中止

Thread aborts in a dynamic library before it has finished executing code

我正在开发一个与 C API 兼容的库。

库中将有一个对象的全局实例,该对象的成员为 std::thread。似乎由于某种原因,当 main returns 和 exit() 被调用时,线程自动 killed/terminated。如果代码在同一项目中使用(直接在可执行文件中而不是通过库),则不会发生这种情况。

我希望下面的示例在无限循环中 运行:while(1) {...}thread.join() 应该互相阻塞。当它与库一起使用时,线程似乎已经 killed/finished 当调用 CThread 的析构函数时。我在这里错过了什么?

CThread.h

#ifndef CTHREAD_H
#define CTHREAD_H

#ifdef EXPORT_C_THREAD
# define EXPORT_CTHREAD __declspec(dllexport) __cdecl
#else
# define EXPORT_CTHREAD __declspec(dllimport) __cdecl
#endif

void EXPORT_CTHREAD testFunc();

#endif

CThread.cpp

#include "CThread.h"

#include <thread>
#include <memory>

class CThread
{
   std::thread m_thread;
   void infiniteLoop()
   {
      while (1) {
         std::this_thread::sleep_for(std::chrono::microseconds(100));
      }
   }
public:
   CThread()
   {
      m_thread = std::thread(&CThread::infiniteLoop, this);
   }

   ~CThread()
   {
      m_thread.join();
   }

};

std::unique_ptr<CThread> cthread;

void testFunc()
{
   cthread = std::make_unique<CThread>();
}

main.cpp(这是在另一个项目中。我正在链接到上面的库。)

#include "CThread.h"

int main()
{
   testFunc();
    return 0;
}

更新

按照建议,我尝试在 DLL_PROCESS_ATTACH 期间初始化 DllMain()- 函数中的 cthread-对象,并在 DLL_PROCESS_DETACH 期间解除分配。由于 DllMain() 函数将获取加载程序锁,因此我必须稍后初始化线程。但是,和以前一样,当 DLL_PROCESS_DETACH 为 "called" 时线程已经中止。 DLL_THREAD_DETACH 不会在退出时被调用。

还有什么建议吗?谢谢!

It seems that of some reason when the main returns and exit() is called, thread is automatically killed/terminated.

这是 exit 函数的预期行为。 IE。它终止了整个进程及其所有线程。

Within the library there will be a global instance of an object which will have a std::thread as a member.

看起来这个对象在编译成.dll时并没有被创建。您可能需要显式导出该对象,这样它就不会被优化掉,因为 .dll 中没有任何内容引用它,例如:

__declspec(dllexport) std::unique_ptr<CThread> cthread;

我可以使用 VS2015 重现此行为。

问题在于 std::unique_ptr<CThread> pthread; 是一个全局对象。指针的删除与 main 线程的执行无关。 dll 的全局数据和主机 exe 的全局数据不会以可预测的方式相互同步。引入线程时还有更多的复杂性,这些线程会在进程退出时停止。

如您所述,将所有代码移动到单个 exe 允许全局数据在 main 线程上适当同步。

要解决这个问题,您可以导出一个 class 样式的 RAII 来管理线程的执行,或者简单地提供一个 "clear thread" 或清理函数并将其导出;该函数将具有以下形式;

void cleanupData()
{
   cthread = nullptr; // block waiting for thread exit
}

为了进一步协助客户端代码,仍然可以提供支持此 "clear thread" 的 RAII class,但不需要从 dll 中导出它。

客户端 RAII 可以根据需要搭载 std::unique_ptrstd::shared_ptr。最简单的形式是这样的;

struct Cleanup {
    Cleanup() = default;
    Cleanup(Cleanup const&) = delete;
    Cleanup& operator=(Cleanup const&) = delete;
    Cleanup(Cleanup&&) = delete;
    Cleanup& operator=(Cleanup&&) = delete;

    ~Cleanup() { cleanupData(); } // clean up the threads...
};

并且这需要与开始使用或从 dll 导入的数据的生命周期相关联。

并用作;

{
    auto cleanup = std::make_unique<Cleanup>();
    testFunc();
    // ...
}

顺便说一句 在析构函数中,在 join() 线程之前,测试以确保它是 joinable()


鉴于更新;这是否可以更改或改进,即一旦执行离开 main 是否可以控制线程? TL;DR,没有。

"the old new thing" 成名的陈峰,引自here

On the other hand, the C runtime library automatically calls ExitProcess when you exit the main thread, regardless of whether there are any worker threads still active. This behavior for console programs is mandated by the C language, which says that (5.1.2.2.3) “a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument.” The C++ language has an equivalent requirement (3.6.1). Presumably, the C runtime folks carried this behavior to WinMain for consistency.

ExitProcess() do是什么?

...

  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.

...

特别是上面的第 1 点和第 3 点,在 std::unique_ptr<CThread> 的析构函数运行时,线程已经停止并发出信号。一旦主线程调用 ExitProcess(),您就不能依赖后台线程执行,它们已经停止并且 OS 正在清理与该进程关联的所有资源。


您在评论中提及;

I will use an IPC mutex within the thread so it's critical that is cleaned up properly.

如果问题是互斥量而不仅仅是线程,那么问题就会稍微安静一些。然后将互斥量提取到全局级别并允许 DllMain(使用 DLL_PROCESS_ATTACH 等)来管理它会更容易。线程将被允许正常访问。可能有一些额外的代码来指示互斥锁的状态,但也可以使用 dll 进行管理。

还请记住,IPC 互斥机制通常包含一个 "abandoned" 状态用于此确切目的。如果互斥量的持有者意外失败并放弃了互斥量,则在尝试访问互斥量时通知互斥量的其余客户端。

来自WIN32 Mutex

If a thread terminates without releasing its ownership of a mutex object, the mutex object is considered to be abandoned. A waiting thread can acquire ownership of an abandoned mutex object, but the wait function will return WAIT_ABANDONED to indicate that the mutex object is abandoned...