6.1 线程基础
(1)线程组成:线程内核对象+线程栈(★进程=进程内核对象+地址空间)
①从内核角度看,线程是一个内核对象,系统用它来存储一些关于线程的统计信息(比如运行时间等)
②从编程角度看,线程是一堆寄存器状态以及线程栈的一个结构体对象。本质上可以理解为一个函数的调用器(其中的寄存器状态用于控制CPU执行,栈用于存储局部变量和函数参数及函数的返回地址)——为什么要使用线程栈的?
线程1 | 线程2 | 备注(使用线程栈的原因分析) |
void func1(){ int a; int b; } | void func2(){ int c; int d; } | 如果不为每个线程分配线程栈,而使用进程中某一共同的栈,设func3先于func4执行,则变量进栈顺序a、b,如果此时执行线程2,则c、d也会进栈,栈顶指针指向d。假设这时func3执行完,要回收栈则会出现将c、d弹出栈的错误。现实中可能会出现更复杂的情况。当然,如果这两个线程严格串行执行,则不会出现这种错误。 |
③线程还可以带有消息队列(GUI线程内部会创建)和APC队列。(但注意这些队列在线程创建时并不同时创建,要在调用GUI函数里才会被创建!)
★进程是线程的容器,线程共享进程的地址空间和资源
(2)什么时候不使用多线程
①当一个算法本身是严格串行化的时候,即计算的每一步都严重依赖前一个操作步骤的结果时,不适合用多线程)。
②当多个功能任务具有比较严格的先后逻辑关系时,不宜采用多线程。因为这涉及到线程同步方法的严格控制,从而可能因加了过多的同步而降低了效率。
③还有一种特殊情况,比如一个服务器需要处理成千上万个客户端连接,不宜使用多线程,因为过多的线程间的切换也会降低效率,这里可以考虑用线程池。
【MessageQueue程序】演示如何在子进程中创建消息队列
#include <windows.h> #include <tchar.h> #include <locale.h> #define WM_MYMSG WM_USER HANDLE hEvent; DWORD WINAPI ThreadProc(PVOID pvParam) { MSG msg = { 0 }; //强制系统创建一个消息队列,注释后可看到该线程没有收到任何消息 PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE); if (!SetEvent(hEvent)) //创建好消息队列后,将事件重置为有信号 return 0; //模拟一个耗时的初始化过程 for (int i = 0; i < 100000000; i++); //一个简化的消息循环 while (GetMessage(&msg,NULL,0,0)){ _tprintf(_T("线程[ID:0x%X]收到消息-0x%04X \t时间(GetTickCount值)- %u\n"), GetCurrentThreadId(),msg.message,msg.time); } //执行到这里表示收到的是WM_QUIT消息 _tprintf(_T("线程[ID:0x%X]收到退出消息-0x%04X \t时间(GetTickCount值)- %u\n"), GetCurrentThreadId(), msg.message, msg.time); return msg.wParam; } int _tmain() { _tsetlocale(LC_ALL, _T("chs")); //创建同步事件 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEvent == NULL) return 0; DWORD dwThreadID = 0; HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID); //Sleep(100); //注释掉此句,以下两条消息可能会收不到 //以下两个消息可能收不到,因为新线程默认没有消息队列 PostThreadMessage(dwThreadID, WM_MYMSG + 1, 0, 0); PostThreadMessage(dwThreadID, WM_MYMSG + 2, 0, 0); WaitForSingleObject(hEvent, INFINITE); //等待子线程创建好消息队列 CloseHandle(hEvent); //消息队列己经建立,此时发送消息将会成功! PostThreadMessage(dwThreadID, WM_MYMSG + 3, 0, 0); PostThreadMessage(dwThreadID, WM_MYMSG + 4, 0, 0); //强制切换到新线程去执行,其实可以不必这样做,这里演示切换线程 //以便让消息到达的时间有差异 Sleep(100); PostThreadMessage(dwThreadID, WM_MYMSG + 5, 0, 0); PostThreadMessage(dwThreadID, WM_MYMSG + 6, 0, 0); //向新线程发送退出消息 PostThreadMessage(dwThreadID, WM_QUIT, (WPARAM)GetCurrentThreadId(), 0); //等待新线程退出 WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); _tsystem(_T("PAUSE")); return 0; }
6.2 主线程
(1)进程的入口函数,从本质上看就是主线程的入口函数。在C\C++下是WinMainCRTStartup
(2)主线程是进程内第1个可执行的线程实体,它可以用来创建别的线程。
(3)主线程退出后,进程也会退出(因为VS嵌入的入口函数会调用ExitProcess终止其它线程的执行。(当自定义入口时,这个行为就要在自定义的入口函数中自行的维护,即自定义入口函数时,那么进程将在最后一个线程退出后,才退出。因此,主线程也未必是最后一个线程!)。
6.3 线程函数(也叫线程入口函数)
(1)线程函数的原型:DWORD WINAPI ThreadProc(LPVOID lpParameter);
(2)线程函数是线程执行的起点,可以执行我们希望的任何任务
(3)当线程函数执行完毕,线程将退出,同进线程栈也会被释放,线程内核对象的使用计数递减,如果计数为0,则删除该线程内核对象。(可见线程内核对象的生命期可能长于线程本身!)
(4)线程函数必须有一个返回值,它会成为该线程的退出代码。其他线程可以用GetExitCodeThread来检查线程是否己终止运行,并进一步判断其退出代码。
(5)线程函数应尽可能使用函数参数和局部变量。因为静态变量或全局变量,多线程时可能因同时访问这些变量而要进行额外的同步。由函数参数和局部变量是在线程栈上创建的,不会出现多线程同时访问的问题。
6.4 CreateThread函数
参数 | 描述 |
psa | 指向一个SECURITY_ATTRIBUTES结构体。使用默认安全属性时传入NULL |
cbStackSize | ①用于指定线程初始时的栈大小,通常传入0即可,此时系统会使用一个合适的大小。默认是1MB(保存在PE文件中!) ②线程栈溢出时,产生异常,这可以用来捕获代码中无穷递归bug。若没限制耗尽进程所有的地址空间。 |
pfnStartAddr | 新线程入口函数的地址(注意:新线程和调用CreateThread函数的线程可以同时被执行,这是windows抢占式的特点) |
pvParam | 传给线程入口函数的参数,可以是一个数值或一个结构体 |
dwCreateFlags | 0——创建后立即执行;CREATE_SUSPENDED——创建后挂起,并不执行 |
pdwThreadId | 得到新线程ID |
返回值 | 成功——线程内核对象的句柄;失败——NULL |
【CreateThread程序】用来说明线程调度是随机的
#include <windows.h> #include <tchar.h> #include <strsafe.h> #include <locale.h> #define MAX_THREADS 10 //最大线程数 DWORD WINAPI MyThreadFunc(LPVOID lpParam); void ErrorHandler(LPTSTR lpszFunction); //自定义线程数据 typedef struct _tagMyData { int val1; int val2; }MYDATA,*PMYDATA; int _tmain() { _tsetlocale(LC_ALL, _T("chs")); PMYDATA pDataArray[MAX_THREADS]; HANDLE hThreadArray[MAX_THREADS]; _tprintf(_T("以下10个线程是按顺序创建的,但线程的调度是随机\n")); //循环创建10个线程 for (int i = 0; i < MAX_THREADS;i++) { pDataArray[i] = (PMYDATA)malloc(sizeof(MYDATA)); pDataArray[i]->val1 = i; pDataArray[i]->val2 = i + 100; hThreadArray[i] = CreateThread(NULL, 0, MyThreadFunc, pDataArray[i], 0,NULL); if (hThreadArray[i] == NULL) { ErrorHandler(_T("CreateThread")); ExitProcess(3); } } //等待所有线程退出 WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); for (int i = 0; i< MAX_THREADS;i++) { CloseHandle(hThreadArray[i]); if (pDataArray[i] != NULL) free(pDataArray[i]); } _tsystem(_T("PAUSE")); return 0; } //线程函数 DWORD WINAPI MyThreadFunc(LPVOID lpParam) { PMYDATA pMyData = (PMYDATA)lpParam; _tprintf(_T("Parameters = %d,%d\n"),pMyData->val1,pMyData->val2); return 0; } void ErrorHandler(LPTSTR lpszFunction) { LPVOID lpMsgBuf; DWORD dwError = GetLastError(); FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,dwError, MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0,NULL); _tprintf(_T("%s failed with error %d:%s"),lpszFunction,dwError,lpMsgBuf); LocalFree(lpMsgBuf); }
【SuspendedCreate程序】用来创建并挂起的线程
#include <windows.h> #include <tchar.h> DWORD WINAPI ThreadFunction(LPVOID lpParam) { _tprintf(_T("Thread(0x%0X) Runing!\n"), GetCurrentThreadId()); return 0; } int _tmain() { //创建并挂起新线程 HANDLE hThread = CreateThread(NULL, 0, ThreadFunction, NULL, CREATE_SUSPENDED, //挂机线程 NULL); _tprintf(_T("Thread Created!\n")); ResumeThread(hThread); Sleep(5); //如果在这里睡眠,将改变ThreadResume与线程函数里输入语句的顺序! _tprintf(_T("Thread Resume!\n")); CloseHandle(hThread); _tsystem(_T("PAUSE")); return 0; }
6.5 终止运行线程
(1)4种终止线程的方式
终止方式 | 描述 |
线程函数返回 | 强烈推荐 ,这是保证所有资源被正确清理的唯一方式!可以确保以下工作正确执行。 ①该函数中的所有C++对象被正确析构。②正确释放线程栈;③把线程退出代码设为函数的返回值;④递减内核对象的计数。 |
ExitThread | ①“杀死主调线程”,操作系统将清理该线程使用的所有操作系统资源(包括线程堆栈) ②可以指定dwExitCode为线程的退出代码;③C\C++资源不会被销毁 |
TerminateThread | ①杀死任何线程;②线程内核对象减1;③不销毁线程堆栈,微软故意这样做,是为了保证其他线程还可以访问被“杀死”线程栈上的值,该堆栈会等到进程结束时才被释放。③该函数是异步的,函数返回时并不保证另一线程被终止。可用WaitForSingleObject判断线程是否终止。 ④将不会通知DLLMain函数某个线程退出,可能导致资源无法释放。 |
进程终止运行时 | ①ExitProcess或TerminateProcess会终止进程中所有进程,同时释放资源。 ②这两个函数就好象为每个线程调用TerminateThread,所以C++对象的析构不会被调用,数据不会回写磁盘…… |
(2)线程终止运行时
①线程拥有的所有用户对象句柄被释放(如窗口和钩子句柄)
②线程退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread参数的退出代码。
③线程内核对象的状态变为触发状态,线程内核对象的使用计数减1。
④如果线程是进程的最后一个活动线程,则进程也被终止。
6.6 线程内幕
(1)线程内部运行机制
①使用计数:CreateThread创建内核对象,使用计数初始值为2(注意:这要求对象的销毁须等线程返回并且关闭从CreateThread返回的对象句柄)
②暂停计数:初始化时设为1。但当线程完成初始化后,系统检查CREATE_SUSPENDED标志是否被设置。如果没被设置,则递减1,从而变为0。这意味着线程可以开始执行了。
③退出代码为STILL_ACTIVE,对象状态为未触发状态。
④分配线程栈,将分别将pvParam和线程函数的地址pfnStartAddr压入栈中。
⑤线程上下文(CPU寄存器状态):保存在线程内核对象中,其中SP指向栈顶(即pfnStartAddr),IP指向RtlUserThreadStart函数(NTDLL.dll中)
(2)RtlUserThreadStart函数执行的操作
/*该函数是新线程真正开始执行的地方(而不是线程函数),虽然该函数有两个参数,有但这并不意味该函数是被其他函数调用的(即不要认为新线程开始执行还要再还上层去找),系统在初始化线程时,这两个参数会被操作系统显式写入线程栈中(但有的CPU架构在传这两个参数时是用寄存器的),所以该函数并没有被其他函数调用,是线程真正开始的地方 */ VOID RTLUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) { __try { //①调用“线程函数”,并传入CreateThread传过来的pvParam参数。 //②退出时调用ExitThread,该函数会使线程内核对象计数递减,并设置退出代码为“线程函数”返回值。 //③因调用的是ExitThread或ExitProcess退出线程的,这意味着线程永远不会退出RtlUserThreadStart函数,它始络在其内部“消亡”,因此该函数 //的返回值为VOID,表示永远不会返回。
//④因为该函数不会返回,而且线程栈中也没有其返回地址(因为没有被其他函数调用),如果在没有强行“杀死”线程的前提下尝试返回, //RtlUserThreadStart将返回到某个随机的内存位置
ExitThread((pfnStartAddr(pvParam)); //回调“线程函数”,并传入pvParam参数 } __except (UnhandleExceptionFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode());//线程函数调用出错,则直接退出进程! } //该函数永远不会返回(因为在ExitThread或ExitProcess中退出了) }
6.7 C/C++运行库注意事项
6.7.1 _beginThreadex的内部实现
(1)_beginThreadex函数
_CRTIMP uintptr_t __cdecl _beginthreadex( void *security, unsigned stacksize, unsigned(__stdcall * initialcode) (void *), void * argument, unsigned createflag, unsigned *thrdaddr ) { //_tiddata是个结构体,是为每线程独享的数据块,(在mtdll.h定义中) //他是从C\C++运行库的堆上分配的,传给_beginthreadex的线程函数和pvParam //参数都保存在这个数据块中,同时该结构还保存C\C++运行库中可能导致线程不安全 //的那些函数中的静态变量(如strok函数使用了依赖于静态变量) _ptiddata ptd; /* 指向每线程数据块指针(使用TLS技术) */ uintptr_t thdl; /* 线程句柄 */ unsigned long err = 0L; /* 从GetLastError()返回的错误代码 */ unsigned dummyid; /* 假的线程ID*/ /* validation section 检查initialcode(线程函数指针)是否为NULL */ _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0); //在C\C++运行库的堆上分配一个_tiddata结构的内存,并赋值给ptd指针 if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) goto error_return; //初始化_tiddata结构体 _initptd(ptd, _getptd()->ptlocinfo); ptd->_initaddr = (void *)initialcode; //线程函数指针 ptd->_initarg = argument; //线程函数的参数 ptd->_thandle = (uintptr_t)(-1); //线程句柄(伪句柄) //确保传入CreateThread函数的thrdaddr(即用来接收线程ID的指针)不为空 if (thrdaddr == NULL) //判断是否需要返回线程ID号 thrdaddr = &dummyid; //调用CreateThread函数来创建新线程 if (thdl = (uintptr_t)CreateThread((LPSECURITY_ATTRIBUTES)security, stacksize, _threadstartex, //在_beginthreadex内部,线程函数的地址被修改成_threadstartex (LPVOID)ptd, //将_tiddata数据块的指针传给线程函数 createflag, (LPDWORD)thrdaddr) //要返回的线程ID指针 == (uintptr_t)0) { err = GetLastError(); goto error_return; } //创建成功,返回线程句柄 return(thdl); //创建线程错误时的处理 error_return: //回收由_calloc_crt()申请的_tiddata块 _free_crt(ptd); //校正错误代码(可以使用GetLastError()得到错误代码) if (err != 0L) _dosmaperr(err); return((uintptr_t)0); //返回值为NULL的无效句柄 } //_threadstartex() -新线程开始的地方 static unsigned long WINAPI _threadstartex(void * ptd) { _ptiddata _ptd; /*从CreateThread传入的线程函数参数 */ //检查动态库中的THREAD_ATTACH调用中是否初始化ptd if ((_ptd = (_ptiddata)__crtFlsGetValue(__get_flsindex())) == NULL) { //将tiddata数据库与线程关联起来 if (!__crtFlsSetValue(__get_flsindex(), ptd)) ExitThread(GetLastError()); //将线程ID保存在_tiddata数据块中。(父线程在调用了CreateThread //以后不能再设置线程ID这个字段了,因为子线程可能己经运行完毕, //并释放了_tiddata数据块) ((_ptiddata)ptd)->_tid = GetCurrentThreadId(); //保存父线程ID _ptd = ptd; } else { _ptd->_initaddr = ((_ptiddata)ptd)->_initaddr; _ptd->_initarg = ((_ptiddata)ptd)->_initarg; _ptd->_thandle = ((_ptiddata)ptd)->_thandle; _freefls(ptd); //如果动态库中己经初始化了ptd,由释放ptd ptd = _ptd; //将ptd赋新的值_ptd } _ptd->_initapartment = __crtIsPackagedApp(); if (_ptd->_initapartment) { _ptd->_initapartment = _initMTAoncurrentthread(); } //调用Helper函数 _callthreadstartex(); //以下将永远不会执行,因为线程最终会终止在_callthreadstartex函数内部! return(0L); } // static void _callthreadstartex(void) { _ptiddata ptd; /* 指向_tiddata指针 */ ptd = _getptd(); //从TLS中获取指向_tiddata的指针 __try { //在这里调用我们的线程函数(函数指针_initaddr字段,参数在_initarg中) //线程函数结束后,将返回值并为_endthreadex的参数来调用_endthreadex以 //便结束线程(注意,很明显,线程会“死”在_callthreadstartex中) _endthreadex( ((unsigned (__CLR_OR_STD_CALL *)(void *))(((_ptiddata)ptd)->_initaddr)) (((_ptiddata)ptd)->_initarg)); } __except (_XcptFilter(GetExceptionCode(), GetExceptionInformation())) { //可能永远不会被执行! _exit(GetExceptionCode()); } /* end of _try - _except */ }
【关于 _beginthreadex说明的几点】
①因为_beginthreadex和_endthreadex是CRT线程函数,所以必须注意编译选项runtimelibaray的选择,使用MT或MTD(MultiThreaded , Debug MultiThreaded)
②每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构。(tiddata结构位于Mtdll.h文件中的VisualC++源代码中)
③传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。传递给该函数的参数也保存在该数据块中
④_beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法
⑤当调用CreatetThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。还有,传递给线程函数的参数是tiddata结构而不是pvParam的地址。即新线程首先执行RtlUserStartAddr,然后跳转进入_threadstartex。
//(2)_endthreadex
void __cdecl _endthreadex( unsigned retcode ) { _ptiddata ptd; /* 指向_tiddata的指针 */ HANDLE handle = NULL; ptd = _getptd_noexit(); //获得指向_tiddata的指针 //清除_tiddata块中的floating-point if (ptd) { if (ptd->_initapartment) _uninitMTAoncurrentthread(); _freeptd(ptd); //释放tiddata结构体,内存被正确释放! } //退出线程 ExitThread(retcode); }
【关于 _endthreadex说明的几点】
①C运行期库的_getptd函数内部调用操作系统的TlsGetValue函数,该函数负责检索主调线程的tiddata内存块的地址。
②然后该数据块被释放,而操作系统的ExitThread函数被调用,以便真正撤消该线程。当然,退出代码要正确地设置和传递。
6.7.2 使用_beginthreadex而不要用CreateThread函数
(1)如果使用CreateThread而不是_beginthreadex来创建线程会发生什么情况?
①当线程调用一个需要_tiddata结构的C\C++运行库函数(如strok)时,这个运行库函数会检查到_tiddata块为NULL,而会自动创建一个与主调线程关联的_tiddata块,这样做的目的是保证该库函数能正常运行。(注意,以后调用的任何C\C++运行库都可以使用这个_tiddata块,而无需重复创建!)。
②但因CreateThread是API函数,不会像_endthreadex那样去销毁这个数据块,因此可能造成内存泄漏。
③如果线程使用了C\C++运行库的signal函数,则会导致整个进程终止,因为CreateThread函数没有为这个函数准备结构化异常处理帧(SEH)
(2)也不要使用_beginthread/_endthread函数(注意,函数名后不带ex)
①_beginthread函数参数少,没有CREATE_SUSPENDED,也不能获取线程ID值
②_endthread是无参的,意味着线程的退出代码被硬编码为0
③_endthread内部会调用CloseHandle来关闭新线程,但这会造成潜在的危险!如:
DWORD dwExitCode;
HANDLE hThread = _beginthread(…); //该函数会使新线程立即运行!
GetExitCodeThread(hThread,&dwExitCode); //但子线程可能在该语句之前就结束了
//但_endthread内部调用了CloseHandle使hThread无效!
CloseHandle(hThread); //这里重复关闭hTread就会出错。
★_endthreadex函数内部不会关闭线程句柄,因此以上代码不会有bug
6.8 了解自己的身份
(1)伪句柄:
功能 | 函数 | 备注 |
获取当前进程的句柄值 | GetCurrentProcess | 永远都是0xFFFFFFFF |
获取当前线程的句柄值 | GetCurrentThread | 永远都是0xFFFFFFFE |
说明:
①伪句柄不会在主调进程句柄表中新建句柄项,故不会影响相应内核对象的使用计数
②如果调用CloseHandle关闭伪句柄,该参数会被忽略,被返回FALSE。调用GetLastError将返回ERROR_INVALID_HANDLE。
③A线程的伪句柄作为参数传递给B线程时,该参数不能正确表示A线程,相反,在B线程中,该句柄其实代表的是B(因为B的伪句柄也是0xFFFFFFFE)。
(2)将伪句柄转换为真实的句柄:DubplicateHandle函数
(3)获取线程、进程运行的CPU时间
①GetThreadTime
②GetProcessTime