Windows运行机理——线程的机制(2)

1. 线程的内存泄漏的主要原因

 

在很多参考书上,都说不要用CreateThread 创建线程、并用CloseHandle来关闭这个线程,因为这样做会导致内存泄漏,而应该用_beginthread来创建线程,_endthread来销毁线程。其实,真正的原因并非如此。看如下一段代码:


HANDLE CreateThread(

//  线程安全属性

        LPSECURITY_ATTRIBUTES lpThreadAttributes,   

//  堆栈大小

        DWORD dwStackSize,    

//  线程函数

         LPTHREAD_START_ROUTINE lpStartAddress,  

          
// 线程参数

         LPVOID lpParameter,    

//  线程创建属性

        DWORD dwCreationFlags,                            

//  线程ID

LPDWORD lpThreadId                             

    );

线程中止运行后,线程对象仍然在系统中,必须通过 CloseHandle 函数来关闭该线程对象。
CloseHandle函数的原型是:
BOOL CloseHandle(

    HANDLE hObject   
//  对象句柄

);

CloseHandle可以关闭多种类型的对象,比如文件对象等,这里使用这个函数来关闭线程对象。调用时,hObject为待关闭的线程对象的句柄。

说用这种方法时内存在泄漏,其实不完全正确。那为什么会引起内存的泄漏呢?因为当线程的函数用到了C的标准库的时候,很容易导致冲突,所以在创建VC的工程时,系统提示是用单线程还是用多线程的库,因为在C的内部有很多的全局变量。例如,出错号、文件句柄等全局变量

因为在C的库中有全局变量,这样用C的库时,如果程序中使用了标准的C的库时,就很容易导致运行不正常,会引起很多的冲突。所以,微软和Borland都对C的库进行了一些改进。但是这个改进的一个条件就是,如果一个线程已经开始创建了,就应该创建一个结构来包含这些全局变量,接着把这些全局变量放入线程的上下文中和这个线程相关起来。这样,全局变量就会依赖于这个线程,不会引起冲突。

这样做就会有一个问题,什么时候这个线程开始创建呢?标准的WindowsAPI是不知道的,因为它是静态的库。这些库都是放在VCLIB的目录内的,而线程函数是操作系统的函数。所以,VCBC在创建线程时,都会用_beginThread来创建线程,再用_endThread来结束线程。这样,它们在创建线程的时候,就会知道什么时候创建了线程,并把全局变量放入某一结构中,让它和线程能关联起来。这样就不会发生冲突了。

很显然,要完成这个功能,首先需要分配结构表把全局变量包含起来。这个过程是在_beginThread时做的,而释放在_endTread内完成。

所以,当用_beginThread来创建,而用CloseHandle来关闭线程时,这时复制的全局结构就不会被释放了,这就有了内存的泄漏。这就是很多资料所说的内存泄漏问题的真正的原因

其实,可以不用_beginThread_endThread这一对函数。如果用CreateThread函数创建,用CloseHandle关闭,那么,与C有关的库就会用全局的,它们会引起冲突。所以,比较好的方法就是在线程内不用标准的C的库(可以使用Windows API的库函数)。这样就不会有什么问题,也就不会引起冲突。例如,字符串的操作函数、文件操作等。

当某个程序创建一个线程后,会产生一个线程的句柄,线程的句柄主要用来控制整个线程的运行,例如停止、挂起或设置线程的优先级等操作。一般来说,当线程启用后,就会用线程的CloseHandle来关闭线程。但在微软的示例程序中,有一个例子创建以后,就马上调用CloseHandle关闭线程的运行。这样做在Windows 98下没什么问题,但在Windows NT下,内核就会出现错误。这是为什么呢?

这是因为虽然线程有关的结构已经释放了,但线程还在运行中,所以程序就会出现错误。那怎么做才能确保正常运行呢?

其实,要正常运行,可以让线程完全结束以后,再调用CloseHandle来释放资源

怎样知道线程完全结束呢?在Windows API中有一类等待线程的命令:

 
DWORD WaitForSingleObject(

  HANDLE hHandle,              
//  handle to object to wait for

  DWORD dwMilliseconds   
//  time-out interval in milliseconds

);

 

DWORD WaitForMultipleObjects(

  DWORD nCount,           
//  number of handles in the handle array

  CONST HANDLE 
* lpHandles,   //  pointer to the object-handle array

  BOOL fWaitAll,                
//  wait flag

  DWORD dwMilliseconds   
//  time-out interval in milliseconds

);

可以用以上两函数,等待线程的结束。如果线程结束,函数就会返回。否则就一直等待,直到指定的时间结束。

还有一种线程根本不会退出,它一直运行着循环的线程。我们就要用中止线程的方法来结束线程的运行,强制把它关闭。强制关闭后,再用CloseHandle来释放结构。


2. 进程管理

 

Win16中,一个正在运行的程序被称为一个任务(task),16位的KERNEL把每一个Win16任务的信息保持在一个叫任务数据库(TDB)的段内,任务数据库的选择器被认为是一个HTASK,通过它可获知正在执行任务的API

Windows 95中,针对32位程序做了什么改进呢?它把一个运行的程序称为一个进程而不是一个任务,每一个进程运行在自己的地址空间内。它们可以看到自己的内存和操作系统,而看不到其他的进程或其他进程的空间。使进程相互之间保持分离的基本原因是防止有问题的进程影响其他进程。

Win32程序中,给WinMainhPrevInstance参数总是为0。不管其他程序是否运行,一般情况下,一个进程自认为系统中只有该程序在运行。当然,如果你确实需要与另外的进程通信(或是去操作另一个进程),也是很容易的,这在编写代码之前就要考虑到。

每一个Windows 95进程在系统中被分配一个单一值。这个值为进程ID,一个程序可以通过GetCurrentProcessID函数获取自己的进程ID。这个进程ID非常近似于一个Win16 HTASKNT中的进程ID分配给系统数据结构,因为典型的进程ID值是数字的,所以Windows 95中的进程ID的值比较高,并且是随机的。一个进程ID可以通过转换获取一个指示器,该指示器指向KERNEL32.DLL,用于跟踪进程的进程数据库结构。

你可能感兴趣的:(windows)