2.线程
2.1线程概念
线程包括下面的必要组件:
易失寄存器、栈和专用存储区被称为线程的环境,这些信息对于每个机器体系结构来说是不同的,这种结构取决于特定的体系结构。尽管线程有它们自己的执行环境,但进程中的每个线程都共享进程的虚拟地址空间和资源。
2.2何时创建线程
每次初始化进程时,系统都会创建一个主线程。对于用Microsoft C/C++编译器生成的应用程序,这个线程首先会执行C/C++运行库的启动代码,后者调用入口点函数(_tmain 或_tWinMain ),并继续执行,直至入口点函数返回C/C++运行库的启动代码,后者最终将调用ExitProcess 。
每个线程都必须有一个入口点函数,这是线程执行的起点。主线程的入口点函数:_tmain或_tWinMain。如果想在进程中常见辅助线程,它必须有自己的入口点函数,形式如下:
DWORD WINAPI ThreadFunc(PVOID pvParam){
DWORD dwResult = 0;
...
return(dwResult);
}
线程函数可以执行我们希望它执行的任何任务。最终线程函数将终止运行并返回。此时,线程将终止运行,用于线程栈的内存也会被释放,线程内核对象的使用计数也会递减。如果使用计数变为0,线程内核对象会被销毁。
2.3 CreateThread函数
如果想创建一个或多个辅助线程,只需让一个正在运行的线程调用CreateThread :
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,//指向SECURITY_ATTRIBUTES结构的指针;
DWORD cbStackSize,//制定线程可以为其线程栈使用多少地址空间;
PTHREAD_START_ROUTINE pfnStartAddr,//线程函数的地址;
PVOID pvParam,//线程函数参数
DWORD dwCreateFlags,指定额外的标志来控制线程的创建;
PDWORD pdwThreadID);//存储系统分配给新线程的ID;
调用 CreateThread 时,系统会创建一个县城内核对象。系统从进程地址空间中分配内存给线程栈使用。新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其它所有线程的栈。
CreateThread 函数时用于创建线程的Windows函数。不过如果写的是C/C++代码,就绝对不要调用 CreateThread。 正确地选择是使用Microsoft C++运行库函数_beginthreadex 。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代 CreateThread 。不管这个替代函数时什么,都必须使用它。
2.4终止运行线程
线程可以通过以下4种方法来终止运行:
2.4.1线程函数返回
设计线程函数时,应该确保在我们希望线程终止运行时,就让它们返回。这是保证线程的所有资源被正确清理的唯一方式。让线程函数返回,可以确保一下正确地应用程序权利工作都得以执行:
2.4.2ExitThread函数
VOID ExitThread(DWORD dwExitCode);
该函数将终止线程的执行,并导致操作系统清理该线程使用的所有系统资源。但是你的C/C++资源(如C++类对象)不会被销毁。所以更好的做法是直接从线程函数返回,不要自己调用 ExitThread 。
ExitThread 是Windows用于“杀死”线程的函数,如果要写C/C++代码,就绝对不要调用 ExitThread 。相反,应该使用C++运行库函数_endthreadex 。 如果使用的不是Microsoft的C++编译器,那么编译器供应商应该提供它们自己的 ExitThread 替代函数。 不管这个替代函数是什么,都必须使用它。
2.4.3TerminateThread函数
BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode);
TerminateThread 是异步函数 ,ExitThread 函数来终止线程,线程的对战会被销毁,而 TerminateThread ,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。
2.5线程终止运行时
一个线程终止时,系统会一次执行以下操作:
2.6线程内幕
系统如何创建和初始化一个线程:
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函数的时候,将发生以下事情:
当一个进程的主线程初始化时,其指令指针指向RtlUserThreadStart,当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数。
2.7C/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);
}
为新线程初始化_tiddata结构之后,接着来看看这个结构如何与线程关联的:
static unsigned long WINAPI _threadstartex (void* ptd) {
// Note: ptd is the address of this thread's tiddata block.
// Associate the tiddata block with this thread so
// _getptd() will be able to find it in _callthreadstartex.
TlsSetValue(__tlsindex, ptd);
// Save this thread ID in the _tiddata block.
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// Initialize floating-point support (code not shown).
// call helper function.
_callthreadstartex ();
// We never get here; the thread dies in _callthreadstartex.
return(0L);
}
static void _callthreadstartex(void) {
_ptiddata ptd; /* pointer to thread's _tiddata struct */
// get the pointer to thread data from TLS
ptd = _getptd();
// Wrap desired thread function in SEH frame to
// handle run-time errors and signal support.
__try {
// Call desired thread function, passing it the desired parameter.
// Pass thread's exit code value to _endthreadex.
_endthreadex (
((unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr))
(((_ptiddata)ptd)->_initarg)) ;
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){
// The C run-time's exception handler deals with run-time errors
// and signal support; we should never get it here.
_exit(GetExceptionCode());
}
}
关于_threadstartex函数,要注意一下几大重点:
再来看看_endthreadex:
void __cdecl _endthreadex (unsigned retcode) {
_ptiddata ptd; // Pointer to thread's data block
// Clean up floating-point support (code not shown).
// Get the address of this thread's tiddata block.
ptd = _getptd_noexit ();
// Free the tiddata block.
if (ptd != NULL)
_freeptd(ptd);
// Terminate the thread.
ExitThread(retcode);
}
对于_endthreadex函数,要注意一下几点:
我们应该避免使用ExitThread函数,因为此函数会“杀死”主调线程,而且不允许它从当前执行的函数返回。由于函数没有返回,所以构造的任何C++对象都不会被析构;它还会阻止线程的_tiddata内存块被释放,使应用程序出现内存泄露(直到整个进程终止)。
我们应该尽量用C/C++运行库函数(_beginthreadex,_endthreadex)而尽量避免使用操作系统提供的函数(CreateThread,ExitThread)。
2.8担心伪句柄
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这两个函数返回到主调函数的进程内核对象或线程内核对象的一个伪句柄。它们不会再主调进程的句柄表中新建句柄。即返回的句柄并不在进程句柄表中有实际的表 项,也不会影响进程内核对象或线程内核对象的使用计数。如果嗲用CloseHandle函数,并传入一个“伪句柄”,CloseHandle只是简单的忽 略此调用。
将伪句柄转换为真正的句柄,DuplicateHandle函数可以执行这个转换,如在进程句柄表中创建线程内核句柄:
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