Win32多线程程序设计(二)
序:
本讲主要介绍产生,监视,退出线程的Win32函数及一个线程的运转过程
1.一个单纯的函数调用和通过启动线程调用的比较
面对一个单纯的函数调用操作,控制权会转移到被调用函数中,执行完毕后再返回到原调用处,如:
void main()
{
int resuilt;
resuilt = square(5);
print(resuilt);
}
int resuilt(int n)
{
…
…
}
2.启动一个线程调用函数
启动线程调用函数时,我们不直接调用函数,而是通过CreateThread()创建一个线程,把要调用的函数的地址传给这个线程,在这个新线程中调用函数,原来的线程继续前进,即函数调用异步的进行了,一旦被调用的函数启动,它就完全独立于原调用端,如:
DWORD WINAPI ThreadFunc(LPVOID)
void main()
{
HANDLE hThread;
DWORD threadId;
hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threaded);
printf(“Thread Running”);
}
DWORD WINAPI ThreadFunc(LPVOID p)
{
…
…
}
补充:
当一个函数被调用时,函数的参数会被传递给被调用的函数,返回值会被返回给调用函数,函数的调用约定是描述参数是怎样传递,返回值是如何返回及调用堆栈是如何清理的
常见的函数调用约定有__stdcall,__cdecl,__pascal
当我们在函数的前面用__stdcall作为修饰符时,函数就会采用__stdcall调用约定,如:
int __stdcall func(void);
Win32 API函数大部分采用__stdcall
#define WINAPI __stdcall
3.多线程带来的问题
1) 程序无法预期
2) 执行次序无法保证
3) 线程对于小的改变有高度的敏感
在多线程程序中,如果采用printf()调试程序时,可能会完全改变多线程程序的行为
4) 创建的线程并不总是立刻启动
4.内核对象,GDI对象和USER对象
在使用c++进行windows编程时,程序员除了管理使用new/malloc动态从堆上分配的内存外,还需要对windows的内核对象,GDI对象和USER对象进行管理,这些对象使用句柄来标示,通过操作这些句柄就使用不同的资源对象,和堆内存一样,程序员也需要管理这些对象资源,以免造成系统资源泄漏
句柄(HANDLE)是WINDOWS用来标示被应用程序建立或使用的对象的唯一整数,句柄实际上是指向某种资源的指针,但与指针又有所不同,指针对应着一个数据所在内存的地址,得到指针就可以自由的修改该数据,但WINDOWS不希望应用程序修改其内部数据结构,故用句柄标示这个内部数据结构,使用户无法修改
GDI对象和USER对象只能有单一的拥有者,不是进程就是线程,GDI对象与绘图相关,USER对象与交互相关
内核对象的直接拥有者是操作系统内核,内核对象可以有多个拥有者即可被多个进程或线程拥有,故内核对象保持一个引用计数以对每个拥有者进行跟踪,这样才能保证内核对象被正确的创建和销毁,当一个进程或线程创建一个内核对象,对象的引用计数为1,如果该对象又被另外的进程或线程共享,每多一个进程,引用计数加1,当一个进程调用CloseHandle()函数后,引用计数减1,如果引用计数变为0,操作系统会销毁该内核对象
5.CloseHanle()的重要性
内核对象的销毁需要借助CloseHanle(),拥有内核对象的进程或线程不在使用该内核对象时调用CloseHanle()减少该内核对象的引用计数,当引用计数减为0时,操作系统销毁此内核对象
GDI对象和USER对象的销毁不需要调用CloseHanle(),每一个GDI对象和USER对象的销毁都有其对象的Destroy或Delete方法
补充:
对内核对象的逻辑上的清除及实体上的清除:
如果一个进程或线程在结束之前,没有针对它所打开的核心对象调用CloseHandle(),则当该进程或线程结束后,操作系统自动把这些对象的引用计数降1,这就是所谓的系统做实体上的清除工作
逻辑上的清除工作是指进程或线程通过CloseHandle()清除所拥有的核心对象
我们一般不要依赖系统做实体上的清除工作,这样容易引起资源泄漏
补充:
线程核心对象和线程的不同之处:
线程的handle指向”线程核心对象”,而不是指向线程本身,当调用CloseHandle()并传给其一个线程handle时,你只不过是表示自己和此核心对象不再有任何瓜葛,CloseHanle()唯一做的事情就是把引用计数减1,如果值变为0,对象就会被操作系统摧毁
线程核心对象的默认引用计数是2,当调用CloseHandle()时,引用计数下降1,当线程结束时,引用计数在降1,只有这两件事情都发生了,不管顺序如何,线程核心对象才会被清除(可以把线程理解为一个特殊的核心对象,其初始的引用计数为2)
创建一个线程的代码段:
int main()
{
HANDLE hThread;
DWORD threaded;
int i;
hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
if(hThread)
{
printf(“Thread launched!”);
CloseHandle(hThread); //该行的目的是,使创建线程的线程与被创建的线程在无瓜葛,这样,当被创建的线程结束后可以自动被销毁
}
Sleep(2000);
return 0;
}
注1:
主线程中创建一个线程后,在主线程中调用CloseHandle(),传入返回的线程HANDLE,表示主线程与新创建的线程内核对象在无瓜葛,因为线程内核对象的默认引用计数为2,调用CloseHanle()后,计数变为1,当线程运行结束后计数变为0,此时线程内核对象被销毁,如果未调用CloseHanle(),即使线程运行结束,如果此时创建该线程的线程未结束,则被创建的线程不能销毁,因为此时计数依然为1,只有当创建该线程的线程销毁后,该线程内核对象才被销毁
注2:
要理解内核对象和GDI对象,USER对象的不同
进一步理解,线程作为内核对象和其他内核对象的不同
6.线程结束代码
线程结束代码的概念:
之前我们提过线程入口函数的固定格式是DWORD WINAPI ThreadFun(LPVOID),这个DWORD类型的返回值就是线程结束代码,我们可以通过GetExitCodeThread()获得指定线程的线程结束代码从而得知线程的状态,也可以通过ExitThread()强制结束一个线程,并指定该线程的结束代码
线程函数有返回值,我们把这个返回值作为线程结束代码,通过获取此返回值,来检验线程的执行状态,或强制结束一个线程并指定这个返回值,之前我们在主线程的最后放一个Sleep(2000)函数,来确保线程函数真正的执行完了,然后结束掉主线程,进而结束整个进程(主线程的地位),实际上,我们无法保证在2000ms内,线程函数真的执行完成了,有可能Sleep()返回时那些线程函数尚未结束
1) 线程函数执行完后,结束这个线程
我们希望存在一个机制可以检验线程函数是否真的执行完成了,如果真正结束了,则结束主线程,否则继续等待,而不是通过让主线程睡眠一个大概的时间段然后直接结束主线程
GetExitCodeThread()给我们提供了一个这样的机制
BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
参数:
hThread:要检测的线程HANDLE
lpExitCode:是一个返回值,指向一个DWORD,返回线程函数的执行状态
返回值:
如果线程已经结束,那么线程的结束代码会放在lpExitCode参数中返回,如果线程尚未结束,lpExitCode返回的值是STILL_ACTIVE
注:
GetExitCodeThread()将传回线程函数(ThreadFunc)的返回值,通过检测lpExitCode的返回值是否为STILL_ACTIVE来判断线程函数是否执行完成
2) 强制结束一个线程
之前我们在线程函数执行完成后结束线程,有时候可能需要强制结束一个线程,而不管线程函数是否执行完成
ExitThread()
VOID ExitThread(DWORD dwExitCode);
参数:
dwExitCode:指定此线程的结束代码
返回值:
无返回值
注:线程入口函数的返回值为DWORD类型是线程的结束代码,我们可以通过GetExitCodeThread()获取这个退出码,如果线程没有退出,则获取的值是一个常量STILL_ACTIVE(259),这样我们可以通过退出码来判断线程是否已退出,也可以通过ExitThread()强制结束一个线程,并指定要结束的线程的结束代码
结束语:
线程某些程序上,是提供了一种异步调用函数的方式