与前面介绍的进程一样,线程也有两部分组成:
1)一个线程内核对象,操作系统用它来管理线程。内核对象中还存储了线程的各种统计信息,包括挂起计数、退出代码等,以便于系统对线程的管理。内核对象中有一个CONTEXT结构,这个结构中存储了线程上一次执行的时候CPU寄存器的状态。
2)一个线程栈,用于维护线程执行时所需的所有函数参量和局部变量。
位于同一个进程的线程共享进程的地址空间且它们共享进程句柄表。因为句柄表是针对进程的。进程需要很多的系统资源,而线程仅仅需要一个线程内核对象和线程栈就可以了,因此线程比进程的开销要小得多。采用多线程来处理问题也是理所当然的了。
采用多线程可以提高程序的执行效率,但是多线程也存在很多问题。在尝试使用多线程时如果处理不当还可能会引入新的问题。如同步问题。
二、编写第一个线程函数
每个线程都需要一个入口点函数。这是线程执行的起点。主线程的入口点函数是_tmain或_tWinmain。如果在进程中创建新线程必须提供自己的入口点函数。
形如:
DWORD WINAPI ThreadFunc(PVOID pvParam){ DWORD dwResult = 0; ... return(dwResult); } |
线程函数可以是任何我们希望它执行的任务,最终线程函数会终止并返回。类似于进程内核对象,如果线程内核对象使用计数变为0,则会被销毁。
1)默认情况下主线程的入口点函数必须命名为main,wmain,WinMain或wWinMain。我们可以通过设置/ENTRY:链接器选项来指定另一个函数作为入口点函数。
2)主线程入口点函数有字符串参数,所以它提供了ANSI/Unicode版本。相反,线程函数只有一个参数,其意义可由我们定义。可以为其传递一个值,也可以将其作为某个数据结构的指针。这需要在线程函数内部做类型转换。
3)线程函数必须返回一个值,它的值传递给ExitThread,作为线程的退出代码。
4)线程函数尽可能地使用局部变量或函数参数,它们是在线程栈上创建的。不太可能被其他线程破坏。使用静态变量或全局变量时其他线程可以访问这些变量,这会导致同步和互斥问题。
三、CreateThread 函数
如果想创建一个或多个辅助线程,只需让一个正在运行的线程调用CreateThread :
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, //指向SECURITY_ATTRIBUTES结构的指针;
DWORD cbStackSize, //制定线程可以为其线程栈使用多少地址空间; 栈空间大小 ,传入0则默认使用内部值 链接器/STACK 可以改变大小 默认1M
PTHREAD_START_ROUTINE pfnStartAddr, //线程函数的地址;
PVOID pvParam, //线程函数参数
DWORD dwCreateFlags, //指定额外的标志来控制线程的创建; CREATE_SUSPENDED 暂停执行
PDWORD pdwThreadID); //存储系统分配给新线程的ID;
例:
DWORD WINAPI FirstThread(PVOID pvParam) { int x=0; //这里的函数,为SecoundThread 的返回值,最好声明为static,防止FirstThread 执行完,SecoundThread 返回时,访问栈越界。 DWORD dwThreadID; HANDLE hThread = CreatThread( NULL, 0 , SecondThread , (PVOID)&x , 0, &dwThread); } DWORD WINAPI SecondThread (PVOID pvParam) { *(int *)pvParam =5; } |
调用 CreateThread 时,系统会创建一个线程内核对象。系统从进程地址空间中分配内存给线程栈使用。新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其它所有线程的栈。
CreateThread 函数时用于创建线程的Windows函数。不过如果写的是C/C++代码,就绝对不要调用 CreateThread。 正确地选择是使用Microsoft C++运行库函数_beginthreadex。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代 CreateThread 。不管这个替代函数时什么,都必须使用它。
四、终止运行线程
线程可以通过以下4种方法来终止运行:
线程函数返回(这是强烈推荐的);
线程通过ExitThread 函数“杀死”自己(应避免);
同一个进程或另一个进程中的线程调用TerminateThread 函数(应避免);
包含线程的进程终止运行(应避免);
五、线程函数返回
设计线程函数时,应该确保在我们希望线程终止运行时,就让它们返回。这是保证线程的所有资源被正确清理的唯一方式。让线程函数返回,可以确保一下正确地应用程序权利工作都得以执行:
线程函数中创建的所有C++对象都通过其析构函数被正确销毁;
操作系统正确释放线程栈使用的内存;
操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值;
系统减少线程的内核对象的使用计数;
六、ExitThread函数
VOID ExitThread(DWORD dwExitCode);
该函数将终止线程的执行,并导致操作系统清理该线程使用的所有系统资源。但是你的C/C++资源(如C++类对象)不会被销毁。所以更好的做法是直接从线程函数返回,不要自己调用 ExitThread 。
ExitThread 是Windows用于“杀死”线程的函数,如果要写C/C++代码,就绝对不要调用 ExitThread 。相反,应该使用C++运行库函数_endthreadex 。 如果使用的不是Microsoft的C++编译器,那么编译器供应商应该提供它们自己的 ExitThread 替代函数。 不管这个替代函数是什么,都必须使用它。
七、TerminateThread函数
BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode); |
TerminateThread 是异步函数 ,ExitThread 函数来终止线程,线程的堆栈会被销毁,而 TerminateThread ,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。
八、线程终止运行时
一个线程终止时,系统会一次执行以下操作:
线程拥有的所有用户对象句柄会被释放;
线程的退出代码从STILE_ACTIVE变为传给ExitThread或TerminateThread函数的代码;
线程内核对象的状态变为已触发状态;
如果线程是进程中的最后一个活动线程,系统认为进程也终止了;
线程内核对象的使用计数递减1;
九、线程内幕
CreateThread 函数的一个调用 导致 系统创建一个线程内核对象,该对象最初的使用计数为2。( 创建线程内核对象加1,返回线程内核对象句柄加1 ),所以除非线程终止,而且 CreateThread 返回的句柄关闭,否则线程内核对象不会被销毁。该线程对象的其它属性也被初始化:暂停计数被设为1,退出代码被设备STILE_ACTIVE(0x103),而且对象被设为未触发状态。
创建了内核对象,系统就分配内存,供线程的堆栈使用。此内存是从进程的地址空间分配的,因为线程没有自己的地址空间。系统将来个值写入新线程堆栈的最上端,如图1所示,即调用的线程函数及其参数。
每个线程都有自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一次执行时,线程CPU寄存器的状态。CONTEXT结构保存在线程的内核对象中。
当线程内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为pfnStartAddr在线程堆栈中的地址。而指令指针寄存器被设为RtlUserThreadStart函数的地址。
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) { __try { ExitThread((pfnStartAddr)(pvParam)); } __except(UnhandledExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode()); } // NOTE: We never get here. } |
线程完全初始化之后,系统检查CREATE_SUSPENDED标志是否已被传给CreateThread函数。如果此标记没有传递,系统将线程的挂起计数递减至0;随后,线程就可以调度给一个处理器去执行。然后,系统在实际的 CPU寄存器中加载上一次在线程上下文中保存的值。现在,线程可以在其进程的地址空间中执行代码并处理数据了。
新线程执行RtlUserThreadStart函数的时候,将发生以下事情:
围绕线程函数,会设置一个结构化异常处理(SEH)帧。这样一来,线程执行期间所产生的任何异常都能得到系统的默认处理。
系统调用线程函数,把传给CreateThread函数的pvParam参数传给它。
线程函数返回时,RtlUserThreadStart调用ExitThread,将你的线程函数的返回值传给它。线程内核对象的使用计数递减,而后线程停止执行。
如果线程产生了一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常,这意味着系统会向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止真个进程,而不是终止有问题的线程。
当一个进程的主线程初始化时,其指令指针指向RtlUserThreadStart,当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数。
十、C/C++运行库注意事项
为了保证C和C++多线程应用程序正常运行,必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联。然后,在调用C/C++运行库函数时,那些函数必须知道去查找主调线程的数据块,从而避免影响到其它线程
编写C/C++应用程序,一定不要调用操作系统的CreateThread函数,相反,应该调用C/C++运行库函数_beginthreadex:
uintptr_t __cdecl _beginthreadex ( void *psa, unsigned cbStackSize, unsigned (__stdcall * pfnStartAddr) (void *), void * pvParam, unsigned dwCreateFlags, unsigned *pdwThreadID) { _ptiddata ptd; // Pointer to thread's data block uintptr_t thdl; // Thread's handle // Allocate data block for the new thread. if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) goto error_return; // Initialize the data block. initptd(ptd); // Save the desired thread function and the parameter // we want it to get in the data block. ptd->_initaddr = (void *) pfnStartAddr; ptd->_initarg = pvParam; ptd->_thandle = (uintptr_t)(-1); // Create the new thread. thdl = (uintptr_t) CreateThread ((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex , (PVOID) ptd, dwCreateFlags, pdwThreadID); if (thdl == 0) { // Thread couldn't be created, cleanup and return failure. goto error_return; } // Thread created OK, return the handle as unsigned long. return(thdl); error_return: // Error: data block or thread couldn't be created. // GetLastError() is mapped into errno corresponding values // if something wrong happened in CreateThread. _free_crt(ptd); return((uintptr_t)0L); } |
对于_beginthreadex函数有以下重点
1) 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆(heap)上分配的。
2)传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。
3)_beginthreadex确实会在内部调用CreateThread,因为操作系统只知道用这种方式来创建一个新线程。
4)CreateThread函数被调用时,传给它的函数地址是_threadstartex(而非pfnStartAddr)。另外,参数地址是_tiddata结构的地址,而非pvParam。
5)如果一切顺利,会返回线程的句柄,就像CreateThread那样。任何操作失败,会返回0。
为新线程初始化_tiddata结构之后,接着来看看这个结构如何与线程关联的:
static unsigned long WINAPI _threadstartex (void* ptd) { // Note: ptd is the address of this thread's tiddata block. // _getptd() will be able to find it in _callthreadstartex. TlsSetValue(__tlsindex, ptd); // Initialize floating-point support (code not shown). // call helper function. static void _callthreadstartex(void) { // get the pointer to thread data from TLS // Wrap desired thread function in SEH frame to // The C run-time's exception handler deals with run-time errors // and signal support; we should never get it here. _exit(GetExceptionCode()); |
关于_threadstartex函数,要注意一下几大重点:
1)新的线程首先执行RtlUserThreadStart(在NTDLL.dll文件中),然后再跳转到_threadstartex;
2)_threadstartex唯一的参数就是新线程的_tiddata内存块的地址;
3)TlsSetValue是一个操作系统函数,它将一个值与主调函数关联起来。这就是所谓的线程局部存储(Thread Local Storage,TLS)。_threadstartex函数将_tiddata内存块与新建线程关联起来;
4)在无参数的辅助函数_callthreadstartex中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个帧处理着与运行库有关的许多事情—比如运行时错误;
5)预期要执行的线程函数会被调用,并向其传递预期的参数。函数的地址和参数会被保存在TLS的_tiddata数据块中,并会在_callthreadstartex中从TLS中获取;
6)线程函数的返回值被认为是线程的退出代码;
再来看看_endthreadex:
void __cdecl _endthreadex (unsigned retcode) { // Clean up floating-point support (code not shown).
|
对于_endthreadex函数,要注意一下几点:
1)C运行库的_getptd_noexit函数在内部调用操作系统的TlsGetValue函数,后者获取主调函数的tiddata内存块的地址;
2)然后_endthreadex将此数据块释放,并调用操作系统的ExitThread函数来实际地销毁线程。它会传递并正确设置退出代码;
我们应该避免使用ExitThread函数,因为此函数会“杀死”主调线程,而且不允许它从当前执行的函数返回。由于函数没有返回,所以构造的任何C++对象都不会被析构;它还会阻止线程的_tiddata内存块被释放,使应用程序出现内存泄露(直到整个进程终止)。
我们应该尽量用C/C++运行库函数(_beginthreadex,_endthreadex)而尽量避免使用操作系统提供的函数(CreateThread,ExitThread)。
十一、担心伪句柄
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这两个函数返回到主调函数的进程内核对象或线程内核对象的一个伪句柄。它们不会再主调进程的句柄表中新建句柄。即返回的句柄并不在进程句柄表中有实际的表项,也不会影响进程内核对象或线程内核对象的使用计数。如果嗲用CloseHandle函数,并传入一个“伪句柄”,CloseHandle只是简单的忽略此调用。
将伪句柄转换为真正的句柄,DuplicateHandle函数可以执行这个转换,如在进程句柄表中创建线程内核句柄:
<SPAN style="FONT-SIZE: 14px"> DuplicateHandle( GetCurrentProcess(), // Handle of process that thread // pseudohandle is relative to GetCurrentThread(), // Parent thread's pseudohandle GetCurrentProcess(), // Handle of process that the new, real, // thread handle is relative to &hThreadParent, // Will receive the new, real, handle // identifying the parent thread 0, // Ignored due to DUPLICATE_SAME_ACCESS FALSE, // New thread handle is not inheritable DUPLICATE_SAME_ACCESS); // New thread handle has same </SPAN> |
相关链接:
《Windows核心编程系列》谈谈Windows钩子
《Windows核心编程系列》谈谈内核对象及句柄的本质
《Windows核心编程系列》谈谈基址重定位和模块绑定
《Windows核心编程系列》谈谈修改导入段拦截API
《Windows核心编程系列》谈谈进程的建立和终止
《Windows核心编程系列》谈谈用户模式下的线程同步
《Windows核心编程系列》谈谈内存映射文件