在Dll中创建线程无法执行

最近遇到问题,在dll中全局初始化CThreadPool,线程一直挂起不执行。
在以下文章中找到答案,转自https://blog.csdn.net/qq_42021840/article/details/105956819:
Windows 核心编程中的原话是这样说的:

 DLL 必须使用DllMain函数来对自己进行初始化。DllMain函数执行的时候,同一个地址空间的中的其他DLL可能还没有初始化,也就是没有调用其他DLL 的DllMain函数,所以我们应该尽量避免去使用从其他DLL中导入的函数。此外,还应该避免在DllMain中调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能会产生循环依赖。

为了更好的理解,这里先介绍一下DllMain 函数:


BOOL WINAPI DllMain(

	_In_ HANDLE hInstance,

	_In_ ULONG  fdwReason,

	LPVOID Reserved

)                   //dll main 函数

{
    
    

	printf("%p\r\n", hInstance);

	switch (fdwReason)

	{
    
    

	case DLL_PROCESS_DETACH:  //0

	{
    
    

		break;

	}

	case DLL_PROCESS_ATTACH: //1

	{
    
    

		break;

	}

	case DLL_THREAD_ATTACH: //2

	{
    
    

		break;

	}

	case DLL_THREAD_DETACH: //3

	{
    
    

		break;

	}

	}

	return TRUE;

}

hInstance:该DLL示例的句柄。这个值表示一个虚拟的地址,DLL的文件映像就储存在这个位置。
fdwReason:表示调用入口点函数的原因/
Reserved:如果DLL是隐式加载的,那么该值不为零,否则为0

DLL_PROCESS_ATTACH 1
当系统第一次将一个DLL映射到进程的地址空间的时候,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_ATTACH。若在第一次映射之后,调用LoadLibrary来载入一个已经映射过的DLL后,操作系统只会递增该DLL的引用计数,并不会调用DllMain。
系统中的某个线程必须负责执行DllMian函数中的代码。创建新的线程的时候,系统会分配进程地址空间并将.exe文件的映像映射到进程的地址空间中。然后,系统将创建进程的主线程,并用这个主线程来调用每个DLL的DllMain函数,同时传入DLL_PROCESS_ATTACH。当所有的已经映射的DLL 都完成了DllMain的调用,那么系统就会让主线程取开始执行.exe的C/C++运行时的启动代码,然后执行.exe的入口点函数(main或 WinMain)。

DLL_PROCESS_DETACH 0
当系统将一个DLL从进程的地址空间中撤销映射时,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_DETACH。如果当DLL_PROCESS_ATTACH时,返回是False,那么将不会有DLL_PROCESS_DETACH的通知。
如果撤销映射的原因是因为进程要被终止,那么调用和ExitProcess函数的线程将负责执行DllMain函数。
如果撤销映射的原因是因为进程中的一个线程调用了FreeLibrary,那么发出调用的线程将执行DllMain函数中的代码。并在DllMian处理完DLL_PROCESS_DETACH通知之前,线程是不会返回的。
注意:
Dll可能会阻碍进程的终止。只有当每个Dll都处理完DLL_PROCESS_DETACH通知之后,操作系统才会终止进程。
如果进程终止是因为TerminateProcess,那么系统不会用DLL_PROCESS_DETACH来调用DllMian。

DLL_THREAD_ATTACH 2
当进程创建一个线程的时候,系统会检测当前映射到该进程地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。新建线程负责执行所有的DLL的DllMain函数中的代码。只有当所有DLL都完成了对该通知的处理之后,系统才会让新线程开始执行它的线程代码。(这也就是出现线程死锁的问题所在)
新建线程只会去调用已经被映射到系统进程空间的DLL中的DllMain函数。也就是说,当一个DLL映射到进程地址空间的 时候,已经存在的线程是不会调用该DLL的DlllMain函数的。
注意:
进程是不会让进程的主线程去调用DLL_THREAD_ATTACH值来调用DllMai函数的,在进程创建的时候,被映射到进程地址空间的任何DLL会收到DLL_PROCESS_ATTACH通知,而不是DLL_THREAD_ATTACH的通知。

DLL_THREAD_DETACH 3
当线程终止的首选方式就是让它的线程函数返回。这回使得系统调用ExitThread来终止线程。ExitThread告诉系统该线程想要终止,但系统不会立即终止,而会让这个线程用DLL_THREAD_DETACH来调用所有已经被映射DLL的DllMain函数。
注意:
DLL可能会妨碍线程的终止,只有当每个DLL都处理完DLL_THREAD_DETACH之后,系统才会真正的终止线程。
进程中的一个线程调用LoadLibrary来载入DLL这使得系统会用DLL_PROCESS_ATTACH来调用DLL的DllMain。当载入该DLL的线程退出的时候,会用DLL_THREAD_DETACH来调用DllMain函数。

在简单的了解DllMain的工作机制后,来分析一个为什么不能在DllMain中创建线程。

你先是想一下这样的情况:

一个进程有两个线程A和B,进程地址空间还映射了一个DLL.dll的DLL。两个线程都调用CreateThread来创建新的线程C和D。
当线程A调用CreateThread来创建线程C的时候,系统会用DLL_THREAD_ATTACH来调用DLL.dll中的DllMian函数,当新建线程C执行DllMain中的代码的时候,线程B调用CreateThread来创建线程D的时候,系统也必须调用DLL.dll中的DllMain函数,但是这次是让要让线程D来执行DllMain。
这个时候系统就会对DllMain函数的调用序列化,它会将线程D挂起,知道线程C执行完DllMain中的代码并返回为止。

这由于会将线程挂起等待的原因,会让在DllMain中创建线程会导致线程死锁的问题。

先看一下有问题的代码:

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
    
    
	HANDLE ThreadHandle = NULL;
	DWORD  ThreadID = 0;
	switch (ul_reason_for_call)
	{
    
    
	case DLL_PROCESS_ATTACH:
	{
    
    
		setlocale(LC_ALL, "chinese");
		BOOL v1;
		MessageBox(0, _T("Dll加载成功"), 0, 0);
		
		ThreadHandle = CreateThread(NULL, 0, ThreadProcedure_1, NULL, 0, &ThreadID);
 
 
		WaitForSingleObject(ThreadHandle, INFINITE);
		CloseHandle(ThreadHandle);
		break;
	}
	case DLL_THREAD_ATTACH:
		MessageBox(0, _T("DllDLL_THREAD_ATTACH"), 0, 0);
 
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

原因是:

  1. 当DllMain收到DLL_PROCESS_ATTACH的时候,会创建一个线程。系统必须要用DLL_THREAD_ATTACH来再次调用DllMain函数。但是在老线程创建新线程的时候,会导致向新线程的DllMian发送DLL_PROCESS_ATTACH通知,由于老线程暂时没有对DLL的初始化,也就是对DllMian的调用没有完成,系统就会将新线程挂起,直到老线程完成调用,才会唤醒。但是老线程调用了WaitForSingleObject来等到新线程的执行,此时新线程已经被挂起在等到老线程执行完毕,但老线程也在等到新线程执行完毕,所有就发生了死锁的情况,两个线程都在互相等待对方的执行结果。

《Windows核心编程》书中提到了DisableThreadLibraryCalls函数,这函数是不让系统像某个指定DLL的DllMain函数发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知,其实这样也不并不能解决问题。

原因是:

当系统创建进程的时候,会同时创建一个锁。每个进程都有自己的锁,多个进程不会共享同一个锁。当进程中的线程调用映射到这个进程空间中的DLL的DllMain函数时,会通过这个锁来同步各个线程。

在程序调用CreateThread的时候,系统首先会创建线程内核对象和线程栈。然后系统内部调用WaitForSingleObject函数,并传入进程的互斥量对象句柄。当新线程得到互斥量所有权后,系统才会让新线程用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。只有这个时候,系统才会调用ReleaseMutex来放弃进程的互斥量所有权。由于系统时以这种方式运作的,所有添加DisableThreadLibraryCalls调用并不能防止线程锁。

所以解决方法就是不要调用WaitForSingleObject。

猜你喜欢

转载自blog.csdn.net/sinat_36391009/article/details/108764282
今日推荐