(3)线程
多线程的优点:可以让用户同时体验应用程序的多种服务。
多线程的缺陷:在多线程访问一共享资源的时候会发生冲突。
和进程一样,线程也由两部分组成。
(1)内核对象:一种数据结构,管理和存储线程的有关信息。
(2)线程堆栈:维护线程运行过程中需要的内存空间。
同一个进程中的线程共享进程的地址空间,共享进程句柄表,共享其他线程的线程堆栈。
主线程的入口点函数是_tmain或_tWinmain。如果在进程中创建新线程必须提供自己的入口点函数。创建的线程必须有一个进入点函数,如下:
DWORD ThreadProc(PVOID pvParam){
int temp=(int)pvParam;
//其中写我们需要线程执行的任务
return 0;
}
线程函数只有一个参数,这个参数可以由我们自己定义。在结束时必须返回一个值,传递给ExitThread,作为函数的退出代码。
在线程函数中如果没有特殊需求,尽量使用作用域只在函数内有效的变量,定义静态变量所有线程都可以引用有可能出现线程间的同步问题。
线程创建函数。
HANDLE CreateThread(
//指明内核对象的安全属性
PSECURITY_ATTRIBUTE psa,
//cbStack参数指定线程栈的大小
//主进程的cbStack在进程创建时会自动通过调用CreateThread创建一个主进程,
//可以使用链接程序/STACK开关来控制这个值。/STACK:[reserve],[commit]
//reserve设置系统应该为线程堆栈保留的地址空间量,默认1MB。
//commit设置默认保证用于堆栈存储的物理存储器的容量,默认1页。
//主动调用CreateThread创建非主线程,如果值不为0就将所有的存储器保留并分配给线程
//保留空间是/STACK的值和cbStack中较大的一个。
//如果传入0则将链接程序嵌入.exe文件的/STACK确定。
//保留空间用于限制堆栈的上限,防止无限递归。
DWORD cbStack,
//pfnStartAddr参数指定线程函数的地址
PTHREAD_START_ROUTINE pfnStartAddr,
//pvParam是传给线程函数的参数
PVOID pvParam,
//dwCreateFlags参数控制线程的创建后的行为
DWORD dwCreateFlag,
//pdwThreadID参数存储线程的ID
PDWORD pdwThreadID
);
线程的结束方式:
1:线程函数返回。
这一种方法是最安全的方法,线程函数自己结束会通过调用ExitThread来调用C++对象的析构函数来释放空间。
2:线程调用ExitThread杀死自己。
此种方式也会释放所有的操作系统的资源,只是所有的C++对象的析构函数不会被调用。
3:其他线程调用TerminateThread。
此方式会把线程直接杀死,可以杀死任何线程,不限于自己,其所占的资源不会被清理,只能等待进程结束清理空间时会清理。TerminateThread为异步方法,必须调用WaitForSingleObject或类似的函数来监听。
4:进程终止导致线程终止。
线程终止运行时,线程所拥有的所有用户对象句柄会被释放。但是也是没有正常调用C/C++的析构函数,数据没转至磁盘
线程终止运行时的操作:
1:线程拥有的所有用户对象被释放
2:退出代码从STILL_ACTIVE改为传给ExitThread或TerminateThread的代码
3:线程对象变为已通知
4:如果是进程中最后一个线程则进程终止
5:线程内核对象引用减一
//获取线程退出代码函数,来判断线程是否已经终止运行。
//也可以使用WaitForSingleObject来监听线程内核对象。
BOOL GetExitCodeThread(HANDLE hThread,PDWORD pdwExitCode);
使用计数:CreateThread函数创建线程内核对象,该对象的最初使用计数为2。一个是该函数返回的句柄对该内核对象的引用,一个是线程本身也占有一个引用。
线程上下文:线程私有的一组寄存器用于保存保存当前线程上一次执行时CPU寄存器的状态,保存在线程内核对象的CONTEXT结构中,线程初始化时CONTEXT结构中的堆栈指针SP设为pfnStartAddr即线程执行函数,指针寄存器IP设置为BaseThreadStart函数,即线程开始是从BaseThreadStart函数开始的。
暂停计数:在线程初始化完后根据是否传递CREATE_SUSPENDED标志来判断是否暂停,如果没有传递则置为0。(初始为1)
已通知标志:初始为未通知状态。
注释:
主线程初始化时调用的是BaseProcessStart函数,该函数和BaseThreadStart函数类似,唯一差别是没有引用pvParam参数。
为了让C/C++创建新线程时有自己的独立的数据块,防止多个进程之间的影响,我们必须调用C/C++多线程运行库函数 _beginthreadex 函数来创建进程。该函数只有多线程库有。
_beginThreadex函数的参数列表跟CreateThread一样,但是参数名称和类型并不完全一样。
在_beginthreadex内部,申请了_tiddata内存块,在C/C++运行堆栈中独立分配的,用于存储局部的数据。
线程函数的地址和参数保存在_tiddata内存块中。
整体流程:
_beginthreadex
1:每个线程获得由C/C++运行库分配的tiddata内存结构。
2:线程函数保存在tiddata中
3:内部调用CreateThread
4:调用CreateThread时通知通过调用_threadstartex来执行线程,传递的参数是tiddata地址
5:执行完返回句柄
_threadstartex
1:新线程从BasethreadStart函数执行,然后转移到_threadstartex
2:tiddata地址作为唯一参数
3:TlsSetValue,是个操作系统函数,用于将一个值与调用线程关联起来。称为线程存储器TLS
4:SEH帧放置在需要的线程函数周围,用来处理与运行库相关的事情
5:调用必要的函数,传递必要的参数(tiddata的地址)
6:返回值为线程的退出代码,返回至_endthreadex
_endthreadex
1:调用TlsGetValue函数获得线程tiddata地址
2:释放该数据块,调用ExitThread销毁线程
常见用CreateThread函数创建线程在大部分情况下也是可以的,在C/C++运行库函数需要用到_tiddata内存块时如果没有会主动创建和关联,只是在后面通过ExitThread退出时内存块不会被释放。另一个问题是结构化异常没有就绪,当使用C/C++运行库的signal函数时将会导致进程终止。
建议使用_beginthreadex和_endthreadex配合使用。
千万不能与CreateThread或ExitThread、TerminateThread混用。
GetCurrentProcess和GetCurrentThread可以获得伪句柄前者返回-1后者返回-2,不能跨进程访问因为-1,-2在句柄表中本就是不存在的。可以通过DuplicateHandle来实现复制句柄,并在不使用时要用CloseHandle关闭句柄。
避免使用_beginthread和_endthread来创建线程,限制较多,_endthread会自动关闭句柄,造成不方便。