Windows核心编程 第六章 线程基础
欢迎转载
转载请注明出处:http://www.cnblogs.com/cuish/p/3145214.html
1、 线程的组成
① 一个是线程的内核对象,操作系统用它管理线程。系统还用内核对象来存储线程统计信息
② 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。
线程 == 内核对象 + 线程栈;
进程是有惰性的,CPU调度是线程,进程永远不会被调度,进程好比一个公司,线程就是每个干活的业务部门。 进程可以看作是线程的容器(作业是进程的容器),线程要在进程的地址空间中执行代码和处理数据。
进程需要更多的系统资源,为一个进程创建一个虚拟的地址空间需要大量的系统资源。 由于.exe和.dll文件要加载到一个地址空间,所以还需要用到文件资源,而线程需要的系统资源要少的多。
线程只有一个内核对象和一个栈,线程需要的内存少很多。
2、 何时应该创建线程
线程并不是越多越好,线程越多需要的系统资源就越多。
进程初始化时系统会创建一个主线程à执行C/C++运行库的启动代码à启动_tmain或_tWinMain并继续执行à直至入口点函数返回C/C++运行库的启动代码à调用ExitProcess退出进程。
当需要创建线程时才应该创建线程,否则尽量不要创建线程。
3、 何时不应该创建线程
几乎在所有应用程序中,所有UI用户界面组件(窗口)都应该共享同一个线程,同一个窗口的所有子窗口都应该有同一个线程来创建。
用户界面线程的优先级通常高于工作线程,这样一来用户界面才能快速响应用户的输入。
4、 创建线程
4.1 win32函数来创建线程
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, //安全属性
DWORD cbStackSize, //线程栈大小,默认为1MB,Itanium默认为4MB
PTHREAD_START_ROUTE pfnStartAddr, //线程函数
PVOID pvParam, //线程函数参数
DWORD dwCreateFlags, //线程创建标记,0 或 CREATE_SUSPEND
PDWORD pdwThreadID //线程ID
);
调用Create时,系统会创建一个线程内核对象,这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个数据结构来管理线程。 系统从进程的地址空间中分配内存给线程栈使用。
WIN32 线程函数原型:
DWORD WINAPI ThreadFunc(PVOID pvParam);
DWORD == unsigned long
WINAPI == __stdcall
PVOID == void*
参数:
① psa
一个指向SECURITY_ATTRIBUTES结构的指针,如果希望该线程内核对象可被继承,可以然其中的bInheritHandle成员置为TRUE
② cbStackSize
栈大小,可以用MS编辑器的/STACK开关来控制这个值
/STACK:[reserve] [,commit]
Reserve参数用来设置系统将为线程栈预留多少地址空间,默认是1MB,安腾(Itanium)芯片组上默认大小为4MB。 Commit参数指定最初应为栈预留的地址空间区域调拨多少物理存储空间,默认是一个页面。 随着线程中代码开始执行,线程需要的存储空间可能不止一个页面。 如果线程溢出栈就会产生异常,系统捕捉这种异常并为已预订的空间区域调拨另一个页面(或者在commit参数中指定更多任何大小个页面),这样线程栈就可以根据需要动态增加。
调用CreateThread时如果为此参数传入非零值,那么函数会为线程栈预定空间并为之调拨所需的所有存储空间。 由于所有存储器都已经事先调拨完毕,所以可以保证线程有指定的栈存储器可用。
线程栈大小可以用下面形象的伪代码来表示:
预定空间的大小= (cbStackSize == 0? /STACK指定的大小 : cbStackSize);
递归函数每次在调用自身时都会在内存栈上创建一个新的栈帧,如果系统没有设置栈空间的上线,那么这个递归调用的函数就会永远不停的调用自身,直到该线程所属进程的地址空间分配殆尽。
为线程栈空间设置上限就避免了。
③ pfnStartAddr 和 pvParam参数
前面已经给出了线程函数的原型
关于pvParam参数,需要注意的是:注意传擦的时候该指针所指的变量是否可能会在某个不确定的时间销毁,从而导致子线程中对该变量的访问违例。
如果多个线程访问同一个变量需要根据需要进行线程同步。
④ dwCreateFlags参数
该参数只有两个值可取: 0 或者 CREATE_SUSPEND
0表示线程创建完后立即执行, CREATE_SUSPEND表示线程创建完后挂起,后续可用ResumeThread(HANDLE hThread)运行该线程。
⑤ pdwThreadID 参数
输出参数,该参数表示该线程的ID,如果不关心可传入NULL。
4.2 终止线程
①线程函数返回:保证线程的所有资源都被正确清理的唯一方式
③ ExitThread函数杀死自己(避免)
④ TerminateThread(避免)
⑤ 线程所属的进程终止运行(避免)
线程函数返回时发生如下:
① C++对象通过其析构函数被正确销毁
② OS正确释放线程栈使用的内存
③ OS把线程的退出代码设为线程函数的返回值
④ 系统减少线程的内核对象的引用计数
VOID ExitThread(DWORD dwExitCode);
此函数将终止线程的运行,并导致OS清理该线程使用的所有OS资源,但是C/C++资源(如C++类对象)不会被销毁,因为其析构函数没有机会执行。
与ExitThread函数类似的C/C++运行库函数为_endthreadex函数。
BOOL TerminateThread(
HANDLE hThread, //线程句柄
DWORD dwExitCode //退出代码
)
该函数可以杀死任何线程,该函数为异步的,就是说该函数返回时,其所终止的线程并不一定已经终止,如果要确定该线程是否终止,可以使用WaitForSingleObject或类似函数来确定。
TerminateThread与ExitThread是有区别的,当使用ExitThread来终止一个线程时,该线程的堆栈也会被销毁;但是如果使用TerminateThread来终止一个线程,除了该线程所属的进程终止,那么系统将不会销毁该线程的堆栈。 Microsoft故意以这种方式来实现TerminateThread,原因是当其他线程要引用被【杀死】的线程堆栈上的值时,为了避免引起访问违例,所以不销毁该堆栈。
DLL通常会在线程终止时收到通知,但是,如果线程是用TerminateThread被杀死的,那么DLL将不会收到通知。
线程终止时发生如下:
① 线程创建的所有用户对象句柄被释放,在windows中大多数对象都是由包含了【创建这些对象的线程】的【进程】拥有的。 但是线程的两个用户对象:【窗口】(window)和【挂钩】(hook),一个线程终止时,系统会自动销毁由线程创建或安装的任何窗口,并卸载由线程创建或安装的任何挂钩。 而除了【窗口】和【挂钩】之外的其他用户对象只有在该线程所属进程终止时才被销毁。
② 线程的退出代码从STILL_ACTIVE 变为传给ExitThread或TerminateThread的代码
③ 线程内核对象的状态变为触发状态
④ 如果线程是进程中的最后一个活动线程,那么系统认为进程也该终止了
⑤ 线程内核对象的使用计数减一
线程终止时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都被关闭了。
获得线程的退出代码:
BOOL GetExitCodeThread(
HANDLE hThread, //线程内核对象句柄
PDWORD pdwExitCode //退出代码
);
如果线程没有终止,那么函数就用STILL_ACTIVE(0x103)来填充退出代码,因此可用该函数来判断线程是否退出。
//////////////////代码段///////////////////////////////////////////
#include "stdafx.h"
#include "windows.h"
#include "process.h"
#include <iostream>
using namespace std;
HANDLE g_hThread1 = 0;
HANDLE g_hThread2 = 0;
unsigned int __stdcall MyFunc(void *p)
{
int a = 0;
while (++ a < 300)
{
Sleep(30);
cout<<"\r\n--------------"<<a<<endl;
}
return 0;
}
unsigned int __stdcall MyFuncState(void *p)
{
HANDLE hThread = (HANDLE)p;
while (TRUE)
{
DWORD dwExitCode = 0;
BOOL bRet = GetExitCodeThread(hThread, &dwExitCode);
if (FALSE != bRet)
{
if (dwExitCode == STILL_ACTIVE)
{
cout<<"\r\n$$$$$$$$$$$$$$$$$$$$$$"<<" still acitve"<<endl;
}
else
{
cout<<"\r\n%%%%%%%%%%%%%%%%%%"<<" may be over"<<endl;
break;
}
}
Sleep(20);
} //while
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
unsigned int ret = _beginthreadex(
NULL,
0,
MyFunc,
NULL,
0,
NULL
);
if (0 != ret)
{
g_hThread1 = (HANDLE)ret;
unsigned int ret2 = _beginthreadex(
NULL,
0,
MyFuncState,
g_hThread1,
0,
NULL
);
if (ret2 != 0)
{
g_hThread2 = HANDLE(ret2);
// CloseHandle(HANDLE(ret2));
}
WaitForSingleObject(g_hThread1, INFINITE);
cout<<"\r\n*********************over**********************\r\n"<<endl;
WaitForSingleObject(g_hThread2, INFINITE);
cout<<"\r\n&&&&&&&&&&&&&&&&&&&&&&& thread2 is over&&&&&&&&&&&&"<<endl;
}
else
{
cout<<"thread is not successful"<<endl;
}
return 0;
}
///////////////////////////////////////////////////////////////////
4.3 线程内幕
线程包括一个堆栈和一个内核对象,
线程内核对象包含一个线程上下文和一些其他的统计信息,这些统计信息及初始值包括如下
【Usage count引用计数==2】【Suspend count挂起数量==1】【退出代码ExitCode==STILL_ACTIVE】【触发状态Signaled=FALSE】,初始该对象内核对象的引用计数为2,挂起计数为1,退出代码为STILL_ACTIVE(0x103),而且对象被设为未触发状态。
一旦创建了内核对象,系统就分配内存,供线程的堆栈使用。 系统将两个值【pvParam】和【pfnStartAddr】写入新线程堆栈的最上端。 其中栈指针寄存器SP指向栈顶,即【pfnStartAddr】,指令指针寄存器IP被设为RtlUserThreadStart函数,该函数从NTDLL.dll导出。
每个线程都有自己的一组CPU寄存器,被称为线程上下文(context)。 线程上下文反映了当线程上一次执行时,线程的CPU寄存器的状态。 线程的CPU寄存器全部保存在一个CONTEXT结构中,该结构在winNT.h中定义, CONTEXT结构本身保存在线程内核对象中。
栈指针寄存器SP和指令指针寄存器SP是线程上下文中最重要的两个寄存器。 线程始终在进程的上下文中运行。
线程初始化à初始化完毕à检查CREATE_SUSPEND标志是否被传给了CreateThread函数,如果有则【挂起数量Suspend count】保持为1,否则减一变为0然后调度执行àCPU寄存器加载线程上下文保存的值à线程执行.
函数传参的方式:①栈 ②寄存器
新线程执行RtlUserThreadStart时,发生如下:
① 围绕线程函数,设置一个结构化异常处理帧SHE,以便线程执行期间的异常都能得到处理
② 调用线程函数pfnStartAddr,把pvParam传给pfnStartAddr函数
③ 线程函数pfnStartAddr返回时RtlUserThreadStart调用ExitThread把pfnStartAddr函数的返回值传给ExitThread,内核对象引用计数减一,然后线程停止
④ 如果线程产生一个未被处理的异常,RtlUserThreadStart函数设置的SHE就会处理这个异常,RtlUserThreadStart会调用ExitProcess终止整个进程。
RtlUserThreadStart函数will never return,永远不会返回, because the function ExitThread or ExitProcess will be called, 因为ExitThread或者ExitProcess函数会被调用。 而线程函数却是可以返回的。
RtlUserThreadStartà调用C/C++运行库的启动代码à调用_tmain或_tWinMain函数à_tmain或者_tWinMain返回时àC/C++运行时启动代码调用ExitProcess退出进程。
对于主线程来说:主线程永远不会返回到RtlUserThreadStart。
4.4 C/C++运行库
Microsoft visual studio 附带的C/C++库
LibCmt.lib----库的静态链接release版
LibCmtD.lib---库的静态链接debug版
MSVCRt.lib---导入库,用于动态链接MSVCR80.dll库的release版(默认)
MSVCRtD.lib---导入库,用于动态链接MSVCR80D.dll库的debug版
MSVCMRt.lib---导入库,用于托管/本机代码混合
MSVCMURt.lib---导入库,编译成百分之百的纯MSIL代码
C/C++运行库的变量和函数有:errno, _doserrno, strtok, _wsctok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt, _fcvt等。
4.5 C/C++运行库函数创建线程
Unsigned long _beginthreadex(
Void* security, //安全属性
Unsigned stack_size, //线程栈大小,默认为1MB,Itanium默认为4MB
Unsigned (*start_address)(void*), //线程函数
Void* arglist, //线程函数参数
Unsigned initflag, //创建标记。0 或 CREATE_SUSPEND
Unsigned *thrdaddr //线程ID
)
该函数返回线程内核对象句柄, HANDLE hThread = (HANDLE)_beginthreadex(…);
替换代码中的CreateThread为_beginthreadex:
Typedef unsigned (__stdcall *PTHREAD_START) (void*) //线程函数
#define chBEGINTHREADEX(psa, chStack, pfnStartAddr,
pvParam, fdwCreate, pdwThreadID)
((HANDLE)_beginthreadex(
(void*)(psa),
(unsined )(cbStackSize),
(PTHREAD_START) (pfnStartAddr),
(void*) (pvParam),
(unsigned) (dwCreateFlags),
(unsigned*) (pdwThreadID) ))
_beginthreadex函数在内部调用CreateThread函数,而对于CreateThread中的【线程函数】和【线程函数参数】来说并非是传给_beginthreadex的参数,而是不同的参数:
【线程函数】:_threadstartex
【线程函数参数】:_tiddata数据块地址, 即一个_ptiddata指针,如下:
_ptiddata ptd; //数据块指针
Uintptr_t thdl;
Ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata) );
if(NULL == ptd)
{
Goto error_return;
}
Initptd(ptd); //初始化数据块
Ptd->_initaddr = (void*)pfnStartAddr; //传给_beginthreadex的线程函数指针
Ptd->_initarg = pvParam; //传给_beginthreadex的线程函数参数
Ptd->_thandle = (uintptr_t)(-1); //线程内核对象句柄
Thdl = (uintptr_t)CreateThread((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex, (PVID) ptd, dwCreateFlags, pdwThreadID);
//…codes
对于_beginthreadex函数,需要注意:
① 个线程都有自己的_tiddata数据块,它们是从C/C++运行库的堆(heap)上分配的
② 传给_beginthreadex的线程函数的地址保存在_tdidata内存块中,
_threadstartex的唯一参数就是新线程的_tiddata内存块的地址。
结束线程的C/C++库函数:_endthreadex
Void __cdecl _endthreadex(unsigned retcode)
{
_ptiddata ptd;
Ptd = _getptd_noexit(); //获取_tiddata数据块地址
If(NULL != ptd)
{
_freeptd(ptd); //释放_tiddata数据块
}
ExitThread(retcode);
}
可以看出该函数会释放_tiddata数据块,并且调用ExitThread结束线程,所以他会传递并正确设置线程的退出代码。
思考:点解唔使CreateThread而要使_beginthreadex呢?
C/C++运行库函数尝试取得_tiddata数据块的的地址(通过TlsGetValue),如果为NULL则表明主调线程没有与之关联的_tiddata数据块, 此时C/C++运行库函数会为主调线程分配并初始化一个_tiddata数据块,然后这个数据块会与线程函数关联(通过TlsSetValue)。
这几乎可以顺利运行,但是加入线程使用了C/C++运行库的signal函数,则整个进程都会终止,因为结构化异常处理帧SHE没有就绪, 第二个问题是:加入线程不是通过调用_endthreadex来终止线程,那么数据块_tiddat上就不会被销毁,从而导致内存泄漏。
4.6 绝对不要使用的C/C++运行库函数
Unsigned long _beginthread(
Void (__cdecl *start_address)(void*),
Unsigned stack_size,
Void *arglist
);
局限性较大,没有安全属性, 线程退出代码并硬编码为0。
关于_endthread还有一个很重要的:_endthread在调用ExitThread前会调用CloseHandle关闭该线程内核对象句柄。
如果有其他地方使用类似GetExitCodeThread函数使用线程内核对象句柄作为参数来调用的时候就会有bug。
4.7 补充条款
HANDLE GetCurrentProcess(); //获取当前进程内核对象句柄,伪句柄
HANDLE GetCurrentThread(); //获取当前线程内核对象句柄,伪句柄
如果调用CloseHandle而传入伪句柄,那么返回FALSE, GetLastError返回ERROR_INVALID_HANDLE。
将伪句柄转换为真实句柄:
使用句柄复制函数DuplicateHandle
BOOL DuplicateHandle(
HANDLE hSourceProcess, //源进程
HANDLE hSource, //源句柄(被复制的)
HANDLE hTargetProcess, //目标进程
PHANDLE phTarget, //被复制到的目的句柄
DWORD dwDesiredAccess, //访问方式,如果最后一个参数为DUPLICATE_SAME_ACCESS则忽略此参数
BOOL bInheritHandle, //复制后的此句柄是否允许被子进程继承
DWORD dwOptions //一些选项
);
Eg.
void Ctest_beginthreadexDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码;
HANDLE hProess = GetCurrentProcess();
BOOL ret1 = CloseHandle(hProess);
HANDLE hTargetProcess = 0;
BOOL ret3 = DuplicateHandle(GetCurrentProcess(), hProess, GetCurrentProcess(), &hTargetProcess,
0, FALSE, DUPLICATE_SAME_ACCESS);
HANDLE hThread2 = GetCurrentThread();
BOOL ret2 = CloseHandle(hThread2);
HANDLE hThread = (HANDLE)_beginthreadex(
NULL,
0,
ThreadFunc,
NULL,
0,
NULL);
if (NULL != hThread)
{
CloseHandle(hThread);
}
}
unsigned int __stdcall Ctest_beginthreadexDlg::ThreadFunc( void* p )
{
int num = 0;
return 0;
}