线程由两部分组成:线程内核对象和线程栈。关于内核对象请看windows内核对象简介。
线程内核对象,操作系统用线程内核对象来管理线程,操作系统还用它来存放统计信息。
线程栈,用于维护线程执行时所需的所有函数参数和局部变量,就是C#程序员常说的线程栈和托管堆中的线程栈。
我们知道进程是有惰性的,它的所有工作都是由线程完成的,而进程只是为线程提供场地,线程函数执行我们让它执行的任务,最终线程函数将终止运行并返回,线程将终止运行,线程的内存将被释放,线程内核对象的使用计数将减一,如果线程内核对象的使用计数减为0,线程内核对象将被销毁。
说了这么多,感觉有点虚,我们还是来看看创建线程的函数吧。
CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
PDWORD lpThreadId
);
lpThreadAttributes:用于设置线程的安全性,线程内核对象是否可以被进程的子进程所继承,详细了解我的这篇文章windows内核对象简介。
dwStackSize:用于设置线程栈的大小,默认为1MB
lpStartAddress:线程所执行的函数的地址
lpParameter:线程函数的参数
dwCreationFlags:用于标识线程是否马上执行
lpThreadId:线程ID
创建线程。调用CreateThread时,系统会创建一个线程内核对象,这个内核对象由操作系统管理,当它的使用计数为0时,会被自动销毁。而当前进程的句柄表也会有一项用于纪录线程内核对象,表示对它的一个引用,同时系统会在进程的地址空间中分配一块内存共线程栈使用。新线程可以访问进程句柄表中的所有内核对象,内核对象由系统内核管理,而进程句柄表维护该进程所用到的内核对象的引用,而线程则通过句柄表中内核对象的地址引用内核对象。新线程还可以访问进程的所有内存以及进程中所有线程的栈,如子线程可以很方便的访问主线程的栈。还有一点需要说明的就是,线程内核对象创建完成之后,线程并不会马上执行,因为线程栈的内存分配和初始化时要时间的,等一切就绪后进程才会开始执行。
终止线程。最好让线程函数自动返回,而不应该强制终止线程函数,因为线程函数在返回前还要做些清理工作,如析构对象,回收内存,让操作系统正确释放线程栈使用的内存,如果强制终止关闭线程,可能这些工作就不能正确的执行,很可能就会出现内存泄露。
线程内幕。对CreateThread的调用产生一个线程内核对象,内核对象的使用计数为2,个人认为使用计数为2的原因是:当前进程对它的引用,还有就是保证在线程函数返回是内核对象不用马上自动销毁,因为当前函数返回时内核对象的使用计数肯定要减一,当减为0时内核对象将自动销毁。还要初始化内核对象的其他属性,并将内核对象设为未触发状态,使其随时可以被执行。等内核对象和线程栈内存一切就绪,系统就会将两个值写入新线程栈的最上端,一个是线程函数的参数,一个是线程函数的地址。
每个线程都有自己的一组CPU寄存器,即为线程的上下文。上下文反映了当前线程执行一次时线程CPU寄存器的状态。因为一个线程可能要经过多次的CPU轮询,才能执行完成,在每次CPU时间用完之前保存CPU寄存器的状态,好让下次轮询到达时继续执行。线程CPU寄存器全部都保存在一个CONTEXT结构中,而CONTEXT本身保存在线程内核对象中。指令指针寄存器和栈指针寄存器是线程上下文中两个重要的寄存器,线程始终在线程的上下文中运行,当线程的内核对象被初始化时,CONTEXT结构的堆栈指针寄存器被设为函数在线程栈中的地址,而指令指针寄存器被设为RtlUserThreadStart函数的地址,而函数RtlUserThreadStart则是线程开始的地方。
最后想说的是,建议用_beginthreadex代替CreateThread创建函数。
作者:陈太汉