清理DLL中的线程:_endthreadx () vs TerminateThread()
Cleaning up threads in a DLL: _endthreadex() vs TerminateThread()
由于DllMain的限制(我知道这同样适用于DLL中的全局静态对象构造函数和析构函数),像带有异步文件写入/刷新线程的单例日志记录器这样简单的事情变得太棘手了。单例日志记录器在DLL中,我对加载和卸载这个DLL的可执行文件的影响有限。我可以强制该可执行文件在任何使用之前调用我的DLL初始化函数,因此在初始化函数中,我可以使用临界区来保护一个变量,告诉DLL是否已经初始化或需要初始化。这种方式避免了DllMain的初始化,这会导致死锁,因为我需要从初始化中启动线程,并且线程以DLL_THREAD_ATTACH
的原因调用DllMain
,并且获得与我们在DLL_PROCESS_ATTACH
事件上初始化DllMain
时已经获得的相同的加载器锁。
c++ 11 thread
不能使用,因为这个错误(在msvc++ 2013中未修复)。所以我用_beginthreadex()
,因为CreateThread
文档说:
调用C运行时库(CRT)的可执行文件中的线程应该使用_beginthreadx和_endthreadx函数来管理线程,而不是使用CreateThread和ExitThread;这需要使用多线程版本的CRT。如果使用CreateThread创建的线程调用CRT, CRT可能会在内存不足的情况下终止进程。
但是我无法控制可执行文件,以确保在卸载DLL之前调用DLL中的一些初始化函数。因此,清理的唯一选择是DllMain
的DLL_PROCESS_DETACH
和全局/静态变量的析构函数。问题是,它们是在获得加载器锁的情况下调用的,所以我不能使DLL线程优雅地退出,因为这些线程在正常退出时会尝试用DLL_THREAD_DETACH
调用DllMain
,这将导致死锁(再次加载器锁)。MSDN建议使用TerminateThread()
来处理:
DLL A在它的DllMain中获得一个DLL_PROCESS_DETACH消息,并为线程T设置一个事件,通知它退出。线程T完成它当前的任务,使自己处于一致状态,向DLL a发出信号,然后无限等待。请注意,一致性检查例程应该遵循与DllMain相同的限制,以避免死锁。DLL A终止T,知道它处于一致状态。
所以我害怕使用_beginthreadex()
+ TerminateThread()
对,而不是设计的_endthreadex()
(后者将由线程本身调用,如果线程正常返回)。
tl;dr考虑一个线程从它的入口函数返回,而一个线程在它的函数结束时做了类似Sleep(INFINITE)
的事情等待被终止(即在它获得一致的资源并向终止线程发出准备好了的信号之后)。做一些CRT或c++ 11资源(如thread_local
)等泄漏或损坏等,如果_endthreadex()
不被调用,但TerminatThread()
被调用代替?
OK。首先,让我们讨论几个次要问题:
-
正如David在评论中提到的,你不需要使用_beginthreadx()而不是CreateThread()。类似地,在任何当前支持的Visual Studio和Windows版本上使用ExitThread()或类似的方法代替_endthreadx()也是可以的。
-
尽管MSDN文章说什么,公认的智慧是永远不能使用TerminateThread()。
-
如果您理解加载器锁所隐含的限制,则可以在DllMain的DLL_PROCESS_ATTACH处理中使用CreateThread()。但是,如果您能够使用适当的初始化例程,而不是像您这样使用DllMain,那就更好了。
所以,如果我正确理解了你的情况,可以总结如下:
-
DLL需要一个或多个后台线程
那有点蠢,但那不是你的错。幸运的是,这不是不可能处理的。
如果线程在可执行文件认为它已经卸载了你的DLL之后继续运行是可以接受的,你可以使用FreeLibraryAndExitThread()模式。在初始化函数中,以及在其他任何创建线程的地方,调用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线程了,但是如果您想严格遵循文档中的规则,这将是一种方法。
- 在VS代码中交叉编译Windows与Linux上的MinGW的SDL程序
- 如何为模板化对象创建模板向量?VS正在投掷C3203
- 数据成员SFINAE的C++17测试:gcc vs clang
- 为什么在Windows上的VS 2019和Clang 9中"size_t"在没有标题的情况下工作
- 在for循环中使用auto vs decltype(vec.size())来处理字符串的向量
- 正在VS调试器中监视映射条目
- Confusion: decltype vs std::function
- 将IBM Rhapsody模型集成到VS 2019中
- VS Code "command":"make"与终端窗口中的命令行"make"不同
- 使用VS Code和CMake Tools运行自定义命令
- 修改 VS Code 中的默认C++代码段
- 如何使用c++在VS 2019上运行SQL查询
- vs 2015 constexpr变量不恒定,但与2019相比还好吗
- 完美前进使用 std::forward vs RefRefCast
- 从VS 2015更新3更新到VS2015更新3 d后浮点计算行为不同的原因
- VS 2015 链接错误 无法构建依赖于 libcurl 的项目
- consteval wrapper vs. source_location
- VS Code C++:不准确的系统包括路径错误(wchar.h,boost/lambda/lambda.hpp)
- QStringList vs list<shared_ptr<QString>> 性能比较C++
- VS 2017 使用交叉编译器构建 x64 项目