前面学习了进程,现在来看看线程。进程可以说是一个正在运行的程序的实例,其实它只是一个运行的程序的一个运行环境,可以说是一个监控者,它负责程序的初始化, 运行期的流程控制,结束时的一些清除工作。而执行程序真正的工作者是线程。现在就来介绍下线程。
线程和进程的组成非常相似,由下面两部分组成
- 线程内核对象。线程内核对象和进程内核对象相似,是用来存储线程的一些统计信息的简单的数据结构
- 线程的堆栈。它用于维护所有线程执行时所用到的参数和局部变量
plus:
- 每个线程必须有一个进入点函数,线程是从进入点函数开始运行。
- 当进入点函数返回时,线程终止运行。堆栈被释放,线程内核对象计数递减1。
- 主线程的进入点函数必须为main,wmain,WinMain,wWinMain之一,而一般线程函数可以用任何名字,但是,注意如果这些函数名要不同,不然系统会认为是某个线程函数的不同实现而已
- 线程函数必须返回一个值,它将成为该线程函数的退出代码
- 尽量多的使用线程函数的参数和局部变量,因为这些都是在线程堆栈中创建的,它不会被其他线程破坏
下面来大概的说下CreateThread函数
原形:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes
DWORD dwStackSize, // initial thread stack size, in bytes
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to returned thread identifier
)
参数:
- lpThreadAttribute:是指向S E C U R I T Y _ AT T R I B U T E S结构的指针。如果想要该线程内核对象的默认安全属性,可以(并且通常能够)传递N U L L。
- dwStackSize:用于设定线程可以将多少地址空间用于它自己的堆栈。这里说下/stack:[reserve][,commit]。这个开关同样可以告诉线程怎么被分配内存。reserve:为这个堆栈保留的大小(默认1MB);commite:为这个堆栈最初分配内存的大小(默认一页)。当dwStackSize不为0的时候,reserve是取dwStackSize和/stack:[reserve]中大的,而commit取/stack:[commit]。当dwStacjSize为0时,就全部去/stack的值
- lpStartAddress AND lpParameter:这两个值是线程函数的地址和线程函数的参数
- dwCreationFlag:可以设定用于控制创建线程的其他标志。它可以是两个值中的一如果该值是0,那么线程创建后可以立即进行调度。如果该值是C R E AT E _ S U S P E N D E D,系统可以完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。
- lpThreadid:是新线程的的ID的存放地址,必须是DWORD的地址。
说完了创建线程,现在来说下终止线程,有四个方法:
- 线程函数返回
- 自己的进程的线程调用ExitThread
- 其他进程或者自己进程的线程调用TerminateThread
- 进程终止
她们当中最好的办法就是:线程函数返回,因为这样可以保证:
- 在线程中创建的c++对象会被调用析构函数,正确清楚
- 操作系统能正确释放线程堆栈占用的内存
- 系统将退出代码设置为函数退出的返回值
- 线程将递减内核对象1
如同进程退出的4个方法一样,调用ExitThread,虽然系统能够保证线程占用的空间能正确清楚,但是c/c++ runtime得不到清除。而TerminateThread则更加的不好,因为它是一个异步函数,调用不能确保线程马上被清除。
好了,说到这里线程的创建,删除都说完了。。但是好象还缺点什么。进程中有一个启动函数,完成了进程的初始化,然后再调用程序员编写的进入点函数。那么线程当然也有自己的初始化环节,下面就来说下线程的初始化。先来看看这个图:
初始化过程:
- 当程序调用CreateThread,系统就创建内核对象,使它的计数为2(为什么为2呢?因为这个线程内核对象的句柄其实有两个,一个是新线程中有一个自己的HANDLE,然后就是调用CreateThread函数的线程中有一个HANDLE,所以要释放线程内核对象必须要将新线程停止运行并且把CreateThread返回的HANDLE关闭)。
- 暂停计数被设置为1,退出码始终为S T I L L _ A C T I V E(0 x 1 0 3),该对象设置为未通知状态。
- 内核对象创建完成,现在就创建线程堆栈。线程堆栈的空间是从进程的地址空间分配而来的
- 系统将pvParam和pfnStartAddr写入线程堆栈。
- 每个线程都有它自己的一组C P U寄存器,称为线程的上下文。该上下文反映了线程上次运行时该线程的C P U寄存器的状态。线程的这组C P U寄存器保存在一个C O N T E X T结构中。C O N T E X T结构本身则包含在线程的内核对象中。
下面再来说下c/c++ runtime library对线程的考虑。单线程应用程序和多线程应用程序其实是调用不同的c/c++ runtime的。其实一共有6个c/c++ runtime library,他们是:
Library Name | Description |
---|---|
LibC.lib | Statically linked library for single-threaded applications. (This is the default library when you create a new project.) |
LibCD.lib | Statically linked debug version of the library for single-threaded applications. |
LibCMt.lib | Statically linked release version of the library for multithreaded applications. |
LibCMtD.lib | Statically linked debug version of the library for multithreaded applications. |
MSVCRt.lib | Import library for dynamically linking the release version of the MSVCRt.dll library. This library supports both single-threaded and multithreaded applications. |
MSVCRtD.lib | Import library for dynamically linking the debug version of the MSVCRtD.dll library. The library supports both single-threaded and multithreaded applications. |
为什么会有不同版本的c/c++ rumtime呢?原因是最早的c/c++ runtime是在1970年开发出来的。那个时候的程序都是单线程,没有考虑到多线程,所以运行多线程应用程序会有问题。其实多线程c/c++ runtime运行程序时需要一个数据结构,这个数据结构与每个线程像联系,记录了程序运行时c/c++ runtime的状态,这样就能保证在多线程运行时,保持c/c++ rumtime的同步性。然后就是,创建线程不会直接调用操作系统的CreateThread,而是调用_beginthreadex,其实这两个函数的原形差不多,所以,只要设置一个函数的宏就可以让你在编写多线程应用程序时和单线程一样了。而_beginthreadex完成了些什么功能呢?下面就来一一例举:
- 在函数里给每个创建的线程分配一个c/c++ runetime堆栈分配的tiddata内存结构
- 传递给_beginthreadex的线程函数的地址以及参数保存在tiddata内存块中
- _beginthreadex从内部调用CreateThread
- 当调用CreateThread时,其实是通过调用_threadstartex而不是线程函数来启动线程的。还有传递给-threadstartex是tiddata而不是线程函数的参数
- 如果成功就返回线程句柄,失败返回NULL
这里说到了一个_threadstartex,那它又做了些什么呢?
- 新线程从BasethreadStart执行,然后转移到_threadstartex
- tiddata被传递给_threadstartex
- 操作系统通过调用TlsSetValue将tiddata与该线程联系起来
- 一个SEH侦被放置在线程周围
- 调用线程函数,并传递参数
- 线程函数的返回值被认为是线程的退出代码,并且调用_endthreadex(注意,并没有直接返回到BaseThreadStart,这样会导致tiddata没有释放,从而内存泄露)
那么_endthreadex又做些什么呢?
- 该函数调用_getptd得到与此想对应的tiddata地址
- 然后该数据块被释放,再调用ExitThread
plus:前面说了,直接调用ExitThread会导致所有创建的c++对象不能撤消,现在再加一条,会导致tiddata不能被释放。还有就是多线程c/c++ runtime能使malloc函数同步,不会出现两个线程同时申请某块内存的事情发生