第六章:线程基础
来源:互联网 发布:ps破解软件下载 编辑:程序博客网 时间:2024/06/05 16:50
1:线程基础
进程有进程内核对象和地址空间,而线程则是线程内核对象和线程栈
之所以线程比进程高效,是因为进程地址空间要占用很多资源用于记录,并且.exe和.dll文件要加载到地址空间,还要用到文件资源
2:CreateThread
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
2.1:LPSECURITY_ATTRIBUTES lpThreadAttributes参数一般为NULL,如果要让此线程内核对象句柄可以被继承,则应创建一个安全描述符并将其bInheritHandle设为true
2.2:dwStackSize为堆栈大小,默认是预留1M,调拨一个页面存储空间,如果调拨的用完,则会产生一个异常,这样会继续调拨一个页面,最多达到1M,这是为了检测死循环BUG
2.3:lpStartAddress是线程函数,LPVOID lpParameter是传入参数,线程函数一般如下
DWORD WINAPI MyThread(PVOID pLparam){return dwResult;//返回线程退出代码,这个代码会被设置到线程内核对象中}
2.4:DWORD dwCreationFlags如果为0,则线程创建好后可以立即被调度,以可以设置为CREATE_SUSPENDED
2.5:LPDWORD lpThreadId可以设置为NULL,表示我们不需要线程ID
3:线程退出
3.1:线程退出的几种方式:
线程函数返回
ExitThread(DWORD dwExitCode);
TerminateThread(HANDLE hd,DWORD dwExitCode);
进程终止
3.2:线程函数返回会执行如下操作
C++对象被析构
线程栈被释放
线程退出代码被设置进线程内核对象
递减线程内核对象使用计数
3.3:线程自己不能阻止自己被TerminateThread()
ExitThread()会销毁线程栈,但TerminateThread()不能销毁线程栈,微软是故意这样设计的,防止一个线程被Terminate后别的线程还需要用它的数据
DLL在一个线程终止时会受到通知,但被TerminateThread()的线程,DLL不会受到通知
3.4:线程终止时,退出代码从STILL_ACTIVE变为函数返回或者调用终止函数时设置的退出代码
线程内核对象变为触发状态,其引用计数-1
如果此线程是进程最后一个线程,则进程被终止
3.5:可以通过GetExitCodeThread(HANDLE hd,DWORD dwExitCode)获得线程退出代码
4:线程内幕
4.1:线程内核对象包括:
使用计数:初始值2
挂起计数:初始值1,如果CreateThread没有设置CREAT_SUSPENDED,则挂起计数-1,线程可以被调度
退出代码:初始值STILL_ACTIVE
触发状态:初始值未触发
一个CONTEXT(上下文)结构,这个结构保存了线程用到的所有寄存器,IP指向NTDLL.DLL中的RtlUserThreadStart();SP指向线程函数;初始化线程栈的时候,会以此压入线程入口参数(pvParam)和函数地址(pfnStartAddr),当程序运行时,变会执行IP所指向的地方的函数(RtlUserThreadStart()),并且由于这两个参数已经被压入线程栈,这等于是给RtlUserThreadStart()传入了这两个参数,RtlUserThreadStart()执行的基本操作如下:
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam){_try{ExitThread((pfnStartAddr)(pvParam));}_except(UnhandledExceptionFilter(GetExceptionInformation())){ExitProcess(GetExceptionCode());}}
4.2:RtlUserThreadStart永远也不会返回
4.3:当创建的线程是主线程时,RtlUserThreadStart运行时会调用C/C++运行时代码,后者会调用入口函数,入口函数返回后会调用C/C++运行库ExitThread()或者ExitProcess();所以对于主线程来说,永远不会返回RtlUserThreadStart函数
4.4:再次说明,对操作系统而言,线程没有主次之分,主线程是针对C/C++运行库而言的
5:C/C++运行库
5.1:C/C++运行库只有多线程版本
5.2:当我们创建一个线程时,应该调用_beginthreadex(),而不应该调用CreateThread(),原因是:C/C++运行库有很多全局变量,多线程访问会出问题,所以C/C++运行库把这些变量(不是全局变量,5.6解释)设置为线程相关的(线程局部存储),这就要为创建的线程分配内存以存放这些变量,但调用操作系统CreateThread()不会分配多余内存,所以必须调用_beginthreadex()
5.3:_beginthreadex()的调用在Threadex.c中,5.2创建的内存块叫做_tiddata,线程函数地址也被保存在这个内存块中,_beginthreadex()会调用CreateThread(),并且传递给CreateThread()的函数入口地址是_threadstartex而非pfnStartAddr函数地址,传递给CreateThread()的参数地址也不是pvParam而是_tiddata内存块
5.4:调用过程为:RtlUserThreadStart()调用_threadstartex(),_threadstartex()从线程局部存储中获得_tiddata内存块,从中获得真正的线程入口函数,调用它,等此函数返回后,调用_endthreadex析构_tiddata内存块,然后调用ExitThread()
5.5:如果手动调用了ExitThread(),则必须手动调用_endthread()析构_tiddata内存块,但不支持这样做
5.6:对于全局变量的处理:全局变量被一个宏重定义,如全局变量error
#define error (*_error)())
调用error实际上是在调用C/C++运行库一个函数,该函数会获取线程相关的数据并返回(这个数据被保存在_tiddata中)
5.7:C/C++运行库会为一些函数同步,如malloc函数
6:如果调用CreateThread()而不是_beginthreadex()
CreateThread()的调用不会分配_tiddata内存块,当要使用内存块中的内容时,C/C++运行库会自动分配_tiddata内存块,并与相应线程关联,但线程终止时,应该显式调用_endthread()析构这个内存块,但一般都会忘记析构这个内存块,所有这种调用方式是不被提倡的
如果有一个DLL连接到C/C++运行库的DLL版本时,这个DLL会在线程析构的时候收到一个DLL_THREAD_DETACH通知,并释放_tiddata内存块,但任然强烈建议不要使用CreateThread()函数
不要调用_beginthread()和_endthread()而应该调用_beginthreadex和_endthreadex()
7:一些有价值的函数
7.1:获得当前进程内核对象句柄和当前线程内核对象句柄:GetCurrentProcess()和GetCurrentThread(),这回返回一个伪句柄,所谓伪句柄是指不会影响其使用计数的句柄,所以不需要调用CloseHandle()递减其引用计数,如果向CloseHanlde()传递了一个伪句柄,CloseHanled()也不会递减其引用计数,这种情况下GetLastError()会返回ERROR_INVALID_HANDLE
7.2:查询进程用时:
FILETIME ftCreationTime,ftExitTime,ftKernelTime,ftUserTiem;GetProcessTimes(GetCurrentProcess(),&ftCreationTime,&ftExitTime,&ftKernelTime,&ftUserTiem);
8:伪句柄转换为真正的句柄
伪句柄指向的是当前进程或线程的句柄,如果写这样一段代码,父进程创建子进程,并将自己的伪句柄传递给子进程,子进程调用GetProcessTimes()想获得父进程执行时间,由于传递的是一个伪句柄,则会得到自己的执行时间
解决办法是用DumplicateHandle()得到一个真正的句柄
8:TLS
慎用TLS,一般在线程栈上分配数据能达到同样的效果,TLS主要解决有大量全局变量或者静态变量的情况
8.1:动态TLS
一个进程创建时,会分配全局使用标志,微软保证至少有64个标志可被使用,但这是动态扩充的,最多能扩充到1000多个
一个线程创建时,会分配同全局使用标志一样多的数组,这是用来存储数据的
使用方法如下
PVOID pElement;//需要存储的数据DWORD dwIndex=TlsAlloc();//存储的位置TlsSetValue(dwIndex,pElement);//存储进线程对应数组PVOID p=TlsGetValue(dwIndex);//获取存储的数据TlsFree(dwIndex);//释放存储位置
TlsSetValue(dwIndex,pElement)中的dwIndex不是TlsAlloc分配的也没有问题,因为没有进行错误检查,这是为了保证速度
TlsAlloc()的一个附加作用是在分配这个存储位置之前,会把每个线程这个位置的数据清零,然后再把这个位置分配出去,这样做的原因是因为:如果加载了一个DLL,设置了该位置,然后释放,如果没有清理过程,下一个使用该位置的DLL也许会有这样的逻辑,检查这里有没有值,如果有值,干什么什么事
8.2:静态TLS
静态TLS不需要调用任何函数,TLS变量定义方法例子如下:
_declspec(thread) DWORD gt_dwSrartTime=0;
_declspec(thread)告诉编译器应该把变量放入自己的段中,gt_dwSrartTime必须是全局或者静态变量
编译器编译时,会将所有上述TLS变量放到他们自己的段中,段名为.tls
编译器链接时,会将上述所有.tls段合并成一个更大的.tls段,并保存在exe或者dll中
当操作系统加载exe或者dll时,会查看.tls段,并为其分配内存块,当访问TLS变量时,操作系统会将其定位到此内存块,这里操作系统会生成额外的代码(3条机器指令),所以静态TLS会让应用程序更大而且更慢
如果新创建了一个线程,系统会再分配一个内存块,新线程会访问新内存块中的TLS变量
这里没有讨论书上DLL和TLS之间的情况
- 第六章:线程基础
- 第六章:线程基础
- Android基础第六天--线程
- windows 核心编程-读后总结 -第六章 线程基础
- 第六章 线程
- 计算机应用基础第六章
- 大神 :第六章.构建基础
- 第六章 6.1 函数基础
- 第六章 面向对象基础
- windows核心编程第六章--线程
- 第六章 <Windows核心编程> 线程基本概念
- C# 线程手册 第六章 线程调试与跟踪
- C#语言 第六部分 线程(一)线程基础(2)
- C#语言 第六部分 线程(一)线程基础(2)
- C#语言 第六部分 线程(一)线程基础(2)
- 《 JavaScript高级程序设计》第六章 DOM基础
- Oracle基础——第六章 查询
- LINUX基础第六章笔记_1
- Lua包(代码文件的组织和调用)_类_继承_多态
- 习语言1.71版
- 言论
- 第五章:作业
- 如何快速删除所有.svn文件夹
- 第六章:线程基础
- Java线程的高级应用
- goole+ VS facebook 竞争IT的第三代霸主
- 线程属性设置
- C# Call another constructor in one constructor
- php操作memcache的使用测试总结
- c# ?? operator
- GOOGLE+ VS FACEBOOK 真人版
- 無題