清理DLL中的线程:_endthreadex()vs TerminateThread()

时间:2016-08-31 05:27:13

标签: c++ multithreading c++11 winapi dll

由于restrictions on DllMain(并且我理解同样适用于DLL中的全局和静态对象构造函数和析构函数),具有异步文件写入/刷新线程的单例记录器这样简单的事情变得太棘手。单例记录器在DLL中,我对可执行文件加载和卸载此DLL的时间影响有限。我可以强制该可执行文件在任何使用之前调用我的DLL初始化函数,因此在初始化函数中我可以使用临界区来保护变量,告知DLL是否已经初始化或者这次需要初始化。这样就避免了从DllMain初始化,这会导致死锁,因为我需要从初始化启动线程,并且线程以DllMain原因调用DLL_THREAD_ATTACH,并获得与已经存在的加载器锁相同的加载器锁我们在DllMain事件DLL_PROCESS_ATTACH初始化时获得。

由于this bug(在MSVC ++ 2013中未修复),因此无法使用C ++ 11 thread。我正在使用_beginthreadex(),因为CreateThread documentation说:

  

调用C运行时库(CRT)的可执行文件中的线程应使用_beginthreadex和_endthreadex函数进行线程管理,而不是CreateThread和ExitThread;这需要使用CRT的多线程版本。如果使用CreateThread创建的线程调用CRT,CRT可能会在内存不足的情况下终止进程。

但我无法控制可执行文件以确保在卸载DLL之前调用DLL中的某些取消初始化函数。因此,清除的唯一选项是DllMain的{​​{1}}和全局/静态变量的析构函数。问题是它们是在获取加载程序锁的情况下调用的,因此我无法使DLL线程正常退出,因为正常退出时的那些线程将尝试使用DLL_PROCESS_DETACH调用DllMain,这将导致死锁(装载机再次锁定)。 MSDN建议使用DLL_THREAD_DETACH来处理此问题:

  

DLL A在其DllMain中获取DLL_PROCESS_DETACH消息,并为线程T设置一个事件,指示它退出。   线程T完成其当前任务,使其自身处于一致状态,发出DLL A信号,并无限期等待。请注意,一致性检查例程应遵循与DllMain相同的限制,以避免死锁。   DLL A终止T,知道它处于一致状态。

所以我害怕使用TerminateThread() + _beginthreadex()对而不是设计的TerminateThread()(如果线程正常返回,后者将被线程本身调用)。 / p>

tl; dr 考虑一个从其入口函数返回的线程,而该线程在其函数末尾执行类似_endthreadex()之类的操作,等待终止(即在它之后)使资源保持一致并向终止线程发出信号,告知它已准备就绪)。如果未调用Sleep(INFINITE),而是调用thread_local,那么某些CRT或C ++ 11资源(如_endthreadex())等是否泄漏或损坏等等?

1 个答案:

答案 0 :(得分:6)

行。首先,我们将介绍几个小问题:

所以,如果我理解你的情况,可以总结如下:

  • 您的DLL需要一个或多个后台线程。

  • 可执行文件会在不发出警告的情况下卸载您的DLL。

这有点愚蠢,但那不是你的错。幸运的是,处理并非不可能。

如果在可执行文件认为已卸载DLL之后线程可以继续运行,则可以use the FreeLibraryAndExitThread() pattern。在初始化函数中,以及创建线程的任何其他位置,调用GetModuleHandleEx()来增加DLL引用计数。这样,当可执行文件调用FreeLibrary()时,如果任何线程仍在运行,则实际上不会卸载DLL。线程通过调用FreeLibraryAndExitThread()退出,保持引用计数。

但是,这种方法可能无法直接满足您的需求,因为它不允许您检测可执行文件何时卸载库,以便您可以通知线程终止。

可能有更聪明的解决方案,但我建议使用辅助DLL。这个想法是帮助DLL而不是你的主DLL跟踪线程引用计数,即,每次创建后台线程时加载辅助DLL,并在每次后台线程退出时卸载它。辅助DLL只需要包含一个调用SetEvent()然后调用FreeLibraryAndExitThread()的函数。

当通知后台线程正在卸载DLL时,它会清理,然后调用帮助程序DLL来设置事件并退出线程。设置事件后,主DLL的分离例程知道该线程不再运行主DLL中的代码。一旦每个后台线程都已完成清理,主DLL就可以安全卸载 - 线程仍在运行并不重要,因为它们正在从辅助DLL运行代码,而不是主DLL。一旦最后一个线程调用FreeLibraryAndExitThread(),辅助DLL就会自动卸载。

再看一遍,一年左右之后,反转它可能更安全:主DLL只包含初始化函数和程序调用的其他函数,加上一个DllMain,表示后台线程退出,并有一个包含其他所有内容的辅助DLL。

特别是,如果辅助DLL包含后台线程所需的所有代码,那么在后台线程仍在运行时卸载主DLL是安全的。

此变体的优点是,当您的后台线程看到要退出的信号时,主DLL是否已经卸载并不重要,因此您的DllMain函数不必在持有加载程序锁定时等待。这样,如果其中一个后台线程无意中执行了需要加载器锁定的操作,则该进程不会死锁。

作为相同想法的变体,如果你真的不想在你的CRT线程上使用FreeLibraryAndExitThread(),你可以在辅助DLL中有一个额外的线程来协调卸载。这个线程将使用CreateThread()启动,并且不会使用任何CRT函数,因此通过FreeLibraryAndExitThread()退出它无疑是安全的。它唯一的责任是在卸载辅助DLL之前等待所有其他线程退出。

不再需要区分CRT和非CRT线程,但如果您想严格遵守规则 - 如文档所述,这将是一种方法。