6.3编写第一个线程函数
针对线程函数的几点说明:
- 线程函数可以使用任何名字。实际上,如果在应用程序中拥有多个线程函数,必须为它们赋予不同的名字,否则编译器/链接程序会认为你为单个函数创建了多个实现函数
- 可以给线程函数传递单个参数,参数的含义由你而不是由操作系统来定义
- 线程函数必须返回一个值,它将成为该线程的退出代码。
- 线程函数(实际上是你的所有函数)应该尽可能使用函数参数和局部变量。
6.4CreateThread函数
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStack,
PTHREAD_START_ROUTINE pfnStarAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadID );
当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。
CreateThread的各个参数
- psa
psa参数是指向SECURITY_ATTRIBUTES结构的指针。如果想要该线程内核对象的默认安全属性,可以(并且通常能够)传递NULL。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构大小,可用sizeof取得
LPVOID lpSecurityDescriptor; //指向一个对象的安全描述符 该安全描述符控制对象的共享 如果为NULL,则该对象使用调用进程的默认安全描述符
BOOL bInheritHandle; //安全描述的对象能否被新创建的进程继承返回句柄 若为TRUE 则新进程继承该句柄
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
- cbStack
cbStack参数用于设定线程可以将多少地址空间用于它自己的堆栈。每个线程拥有它自己
的堆栈。
可以使用链接程序的/STACK开关来控制这个值
/STACK:[reserve][.commit]
reserve参数用于设定系统应该为线程堆栈保留的地址空间量,默认值为1MB。
commit参数用于设定开始时应该承诺用于堆栈保留区的物理存储器的容量,默认值1页。 - pfnStartAddr和pvParam
pfnStartAddr参数用于指明想要新线程执行的线程函数的地址.线程函数的PVParam参数与原先传递给CreateThread的PVParam参数是相同的。 CreateThread使用该参数不做别的事情,只是在线程启动执行时将该参数传递给线程函数。该参数提供了一个将初始化值传递给线程函数的手段。
创建多个线程,使这些线程拥有与起始点相同的函数地址,这是完全合乎逻辑的并且是非常
有用的。 - fdwCreate
fdwCreate参数可以设定用于控制创建线程的其他标志。它可以是两个值中的一个。如果
该值是0,那么线程创建后可以立即进行调度。如果该值是CREATE_SUSPENDED,系统可以
完整地创建线程并对它进行初始化,但是要暂停该线程的运行,这样它就无法进行调度。(CREATE_SUSPENDED)这个参数并不常用。 - pdwThreadID
它必须是DWORD的一个有效地址。
6.5终止线程的运行
一、终止线程的方法:
-
线程函数返回【强烈推荐】
如果线程能够返回,就可以确保如下事项:
1)在线程函数中创建的所有C + +对象均将通过它们的撤消函数正确地撤消。
2)操作系统将正确地释放线程堆栈使用的内存。
3)系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
4)系统将递减线程内核对象的使用计数。 - ExitThread函数【最好不要使用】
VOID EixtThread(DWORD dwExitCode);
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。可以使用 EXitThread的得ExitThread参数告诉系统将线程的退出代码设置为什么。
- TerminateThread函数【尽量避免使用】
BOOL TerminateThread(HANDLE hThread,
DWORD dwExitCode);
ExitThread总是撤消调用的线程, 而TerminateThread能够撤消任何线程。
hThread参数用于标识被终止运行的线程的句柄当线程终止运行时,它的退出代码成为你作为dwExitCode参数传递的值。同时,线程的内核对象的使用计数也被递减。
需要注意的地方:
1)Terminate函数是一部运行的函数,即它告诉系统说你想要线程终止运行,但是,当函数返回时,不能保证线程被撤销。
2)当使用返回或调用ExitThread的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。
-
在进程中终止运行时撤销线程【尽量避免使用】
ExitProcess和TerminateProcess函数也可以用来终止线程的运行。这两个函数会导致进程中的
剩余线程被强制撤消,就像从每个剩余的线程调用TerminateThread一样,这意味着正确的应用程序清楚没有发生,即C++对象撤销函数没有被调用,数据没有转至磁盘等等。
二、线程终止运行时发生的操作
- 线程拥有的所有用户对象(窗口和挂钩)都被释放。
- 线程的退出代码从STILL_ACTIVE改为传递给ExitThread或TerminateThread的代码。
- 线程内核对象的状态变为已通知。
- 如果线程是进程中的最后一个活动线程,系统也将进程视为已经终止运行
- 线程内核对象的使用计数减1
别的线程可以调用GetExitCodeThread来检查由hThread标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码。
BOOL GetExitCodeThread(
HANDLE hThread,
PDWORD pdwExitCode);
退出代码的值在pdwExitCode指向的DWORD中返回。如果调用GetExitCodeThread时线程
尚未终止运行,该函数就用 STILL_ACTIVE标识符(定义为 0x103)填入DWORD。如果该函
数运行成功,便返回TRUE。
6.6线程的一些性质
每个线程都有它自己的一组CPU寄存器,称为线程的上下文。该上下文反映了线程上次运
行时该线程的CPU寄存器的状态。CPU寄存器保存在一个CONTEXT结构中,CONTEXT结构本身则包含在线程的内核对象中。
下面是BaseThreadStart函数执行的基本操作:
VOID BaseThreadStart(PTHREAD_START_ROUTINE
pfnStartAddr, PVOID pvParam)
{
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandExceptionFilter(GetExceptionInformation())){
ExitProcess(GetExceptionCode());
}
}
由于新线程的指令指针被置为BaseThreadStart,因此该函数实际上是线程开始执行的地方。
当执行BaseThreadStart函数时,将出现如下情况:
- 在线程函数中建立一个结构化异常处理(SEH)帧,这样,在线程执行时产生的任何异常情况都会得到系统的某种默认处理。
- 系统调用线程函数,并将你传递给CreateThread函数的pvParam参数传递给它。
- 当线程函数返回时, BaseThreadStart调用ExitThread并将线程函数的返回值传递给它。
该线程内核对象的使用计数被递减,线程停止执行。 - 如果线程产生一个没有处理的异常条件,由BaseThreadStart函数建立的SEH帧将负责处理
该异常条件。通常情况下,这意味着向用户显示一个消息框,并且在用户撤消该消息框时,
BzsethreadStart调用ExitThread,以终止整个进程的运行,而不只是终止线程的运行。
6.7 C/C++运行期库的考虑
若要创建一个新线程,绝对不要调用操作系统的CreateThread函数,必须调用C/C++运行期库函数_beginthreadex:
unsigned long _beginthreadex(
void *securtiy,
unsigned stack_size;
unsigned (*start_adderss)(void*),
void *arglist,
unsigned initflag,
unsigned *thrdaddr);
_beginthreadex函数的参数列表与CreateThread函数的参数列表是相同的,但是参数名和类型并不完全相同。
**_beginthreadex的一些要点:
- 每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构
- 传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。包括传递给该函数的参数
- _beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法。
- 当调用CreateThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。传递给线程函数的参数是tiddata结构而不是pvParam的地址。
- 成功返回句柄,失败返回NULL
关于_threadstartex的一些重点:
- 新线程开始从BaseThreadStart函数执行,然后转移到_threadstartex。
- 到达该新线程的tiddata块的地址作为其唯一参数被传递给_threadstartex
- TlsSetValue是个操作系统函数,负责将一个值与调用线程联系起来。这称为线程本地存储器(TLS)
- 一个SEH帧被放置在需要的线程函数周围
- 调用必要的线程函数,传递必要的参数。【函数和参数的地址由_beginthreadex保存在tiddata块中】
- 必要的线程函数返回值被认为是线程的退出代码。其是先调用C/C++运行期库函数_endthreadex,并传递退出代码。
_endthreadex的一些要点 - C运行期库的_getptd函数内部调用操作系统的TlsGetValue函数,该函数负责检索调用线程的tiddata内存块的地址。
- 该数据块被释放,操作系统的ExitThread函数被调用,以便真正撤销该线程。
6.8 对自己的ID概念有所了解
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这个两个函数返回调用线程的进程的伪句柄或线程内核对象的伪句柄。
DWORD GetCurrentProcessId();
DWORD GetCurrentThreadId();
利用函数DuplicateHandle伪句柄转化为实句柄
BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
PHANDLE phTarget,
DWORD fdwAcess,
BOOL bInheritHandle,
DWORD fdwOptions);