回炉重造之重读Windows核心编程-006-线程

  线程也是有两部分组成的:

  1. 线程的内核对象,操作系统用来管理线程和统计线程信息的地方。
  2. 线程堆栈,用于维护现场在执行代码的时候用到的所有函数参数和局部变量。

  进程是线程的容器,如果进程中有一个以上的线程,这些线程将共享进程的地址空间,操作空间中的数据,执行相同的代码,对相同的数据操作,甚至内核对象句柄(因为它是依托进程而不是线程存在的)。

  所以进程使用的系统资源比线程多的多,线程只需要一个内核对象和一个堆栈。既然线程比进程需要的开销少,因此始终都应该设法用增加线程来解决编程问题。当然这也不是一成不变的,应该懂得权衡利弊。




何时运行线程

  前面的章节已经提到过,当进程被初始化时,系统就要为进程创建一个主线程,用以和CC++运行期库的启动代码一起运行,然后进入入口点函数,然后运行至入口点函数返回并CC++运行期库的启动代码调用ExitProcess为止。

  每个计算机都有一个功能强大的资源,CPU。让它闲置起来是没有道理的,应该让它处于繁忙之中,执行各种各样的工作:

  • 打开Windows2K开始就自带的内容索引服务程序,它能创建一个低优先级的线程,以定期打开磁盘上的文件内容并为之做索引。这样可以大大加快查找文件的效率。
  • 还可以使用磁盘碎片整理软件,使用低优先级线程运行,在系统空项期整理文件碎片。
  • 自动编译源代码文件、实时查看错误和警告信息。
  • 电子表格应用程序能够在后台运算。
  • 文字处理程序能够执行重新分页、拼写、后台打印和语法检查。
  • 文件内容后台拷贝到其他介质中。
  • Web浏览器和其他的服务器通信。

  最重要的是,多线程可以简化用户界面。设计一个拥有多线程的应用程序,可以扩大应用程序的功能。每一个线程都被分配了一个cpu,因此如果计算机有多个CPU,就可以让所有cpu都处于繁忙状态。



不能创建线程的情况

  多线程并发至少有一个问题:代码的重入,数据访问的冲突。

  一般来说一个应用程序有一个用户界面线程用于创建所有窗口,以及一个GetMessage循环。其他的线程都是工作线程,比用户界面线程优先级要低。



编写第一个线程函数

  主线程有对应的入口点,那么线程函数也有个入口函数,作为开始执行第一条代码的地方。和主线程一样,执行代码到结束就释放资源,线程内核对象的引用计数递减。

  • 和主线程不同,线程函数可以是任何名字。
  • 线程函数也可以传递参数。
  • 线程函数必须返回一个值,作为退出代码。
  • 线程函数应该尽可能地使用函数参数和局部变量。如果访问静态和全局的数据,就会有同步的问题。


CreateThread函数 

  调用下面的WindowsAPI就可以创建一个线程:

  

  上面的函数被调用的时候,系统创建一个内核对象,用来管理与线程相关的数据结构,和进程是类似的。系统从进程的地址空间中分配内存,给线程的堆栈使用。新线程的运行环境与创建线程的环境相同。这样使得同一个进程的多个线程之间的通信时相对方便的。

  注意:CreateThread函数是Windows用来创建线程的函数。可是如果你在写CC++代码的话,就不应该使用它了,而是使用VisualC++的运行时函数_beginthreadex。如果不是VisualC++编译器,你的编译器供应商会提供CreateThread的替代函数。反正不能用CreateThread。

  psa参数是指向SECURITY_ATTRIBUTES结果的指针,一般使用默认值。具体的用法请看第三章。

  cbStack参数用于设置线程堆栈大小,可以用链接程序的/STACK:[reserve][.commit]开关控制这个值。reserve参数用来设定系统为线程堆栈保留的地址控件量,默认1MB;commit参数用于设定“应该承诺用于堆栈保留区的物理存储器的容量”,默认值是1页。这个参数即便传递了值,函数仍然会检查链接器已经设置的值,哪个大用哪个。如果把0传进来,就使用Stack开关中设置的值。无论如何,这个值应该有个上限,否则如果存在递归则有可能消耗完所有的资源。

  pfnStartAddrpvParam分别是入口地址和参数。

  fdwCreate参数可以是0或者CREATE_SUSPEND。前者表示线程创建后立即调度,后者表示创建线程后先暂停运行。然而后者并不常用。

  pdwThreadID参数用来存放线程ID。

  



终止线程的运行  

  • 线程函数返回(最好的办法)如果线程可以返回,就可以确保下列事项的实现:
  1. 在现场函数中创建的所有C++对象都能通过它们的析构函数销毁。
  2. 操作系统正确地释放线程堆栈使用的内存。
  3. 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
  4. 系统递减线程内核对象的引用计数。
  • 调用ExitThread函数,线程自行撤销(最好不用)。如果使用,C++资源将不会被回收。实在要用也是用VisualC++提供的_endthreadex,或者你的编译器供应商提供的替代函数。
  • 使用TerminateThread函数(避免使用)。它能撤销线程,线程的内核对象的引用计数也会递减。
  1. TerminateThread函数是异步运行的,想知道线程终止运行,就要调用WaitForSingleObject或者类似的函数。(设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤销的通知,线程不能被正确地清除,并不能防止自己被撤销)
  2. 使用这个函数,系统是不回收这个线程的堆栈资源的。
  3. 线程终止运行的话,DLL通常接受清楚通知。而使用这个函数,DLL就不接受通知了,这就阻挡了适当的清除。
  • 包含线程的进程终止运行(避免使用)
  1. 就像对剩余的每个线程调用TerminateThread一样。显然这意味着正确的应用程序清除没有发生:C++对象的析构函数没有被调用, 数据没有转至磁盘等。


线程如果终止

  • 线程拥有的用户对象全被释放。
  • 线程的退出代码从STILL_ACTIVE传递给ExitThread或者TerminateThread
  • 线程的内核对象变为已通知。
  • 如果是最后一个线程,系统也将进程视为已经终止运行。
  • 线程内核对象的引用计数递减1。

一旦线程不再运行,系统中就没有别的线程能够处理这个线程的句柄,而别的线程可以调用GetExitCodeThread来检查hThread标识的线程是否时间终止运行。如果是这样,就确定它的退出代码:

  

  如果尚未终止运行,函数就返回STILL_ACTIVE标识符(定义为0x103),放进pdwExitCode。如果成功就返回TRUE



线程的其他性质

  SP和IP分别是参数和线程函数的入口!

  

  如果调用CreateThread创建了一个内核对象,下面的事情就会发生:

  1. 这个对象的引用计数是2。
  2. 线程的内核对象的其他属性也被初始化,引用计数被设置为1,退出代码设置为STILL_ACTIVE(0x103),该对象的已通知设置为未通知状态。

  一旦内核对象创建完成:

  1. 系统就从进程的地址空间中,给线程堆栈分配内存。
  2. 系统先后把参数和入口地址写入堆栈。

  每个线程都有自己的一套CPU寄存器,成为线程的上下文,用以反映线程上次运行时寄存器的状态。这些寄存器保存在CONTEXT结构里,这个结构本身则保存在线程的内核对象中。

  ESP和EIP是线程上下文中两个最重要的寄存器。

原文地址:https://www.cnblogs.com/leoTsou/p/12362352.html

时间: 2024-08-30 14:06:59

回炉重造之重读Windows核心编程-006-线程的相关文章

回炉重造之重读Windows核心编程-003-内核对象

内核对象是个比较难理解的概念,问题的根源就在于即使是<核心编程>书中也没有说清楚它的定义,只是不停地举例和描述它的性质,还有如何使用. 盲人摸象,难见全貌.只能尽可能列举它的性质,注意使用了. 引用计数(书中的说法是使用计数)就是内核对象的一个很关键的性质.由于内核对象的拥有者是内核而不是进程,所以只能由内核来做撤销内核对象的操作.而通常一个内核对象不一定只被一个进程使用的,创建或者撤销内核对象,就要看引用计数了.引用计数在内核对象被创建的时候被置为1,被进程访问一次引用计数就递增1.当引用计

回炉重造之重读Windows核心编程-001-错误处理

Windows处理错误靠的是API的返回值,类型不止一种种: VOID,函数不可能失败,Windows API的返回值很少是这个情况. BOOL,如果函数失败,则返回值是0,否则返回是非零值.不要测试返回值是否为TRUE! HANDLE,如果函数失败,则返回值通常是NULL,否则返回一个HANDLE用于操作对象.有的函数是返回INVALID_HANDLE_VALUE的,它被定义为-1,以函数在文档中的说明为标准! PVOID,如果函数失败,则返回NULL,否则返回内存块的地址. LONG/DWO

回炉重造之重读Windows核心编程-007-线程的调度、优先级与亲缘性

Windows被设计成一个抢占式的操作系统,用某种算法来确定哪些线程应该在何时被调度和运行多长时间.每隔20ms左右,Windows就要查看当前所有线程的内核对象,找到可以被调度的一个,将它加载到CPU寄存器中.这个操作成为上下文切换.Windows实际上保存了一个记录,说明每个线程获得了多少次运行的机会.使用MicrosoftSpy++这个工具可以了解这个情况. 一个线程随时可以停止运行,一个线程可以进行调度.可以对线程进行一定程度的控制,但是不能太多.不能保证一个线程做任何事. 7.1暂停和

回炉重造之重读Windows核心编程-011-线程池和其他异步方式

线程池的使用 多线程应用程序很难设计,有两大难点,一是要管理线程的创建和撤销,再就是要对线程访问资源时实施同步.同步的工具有几个了.为了应对线程频繁地创建和撤销,线程池这个方案被放上了台面.Windows2000提供了一些新的线程池函数,使得线程的创建.撤销和基本管理更加容易.线程池的实现不拘一格,只要遵循以下的要点: 异步调用函数. 按照规定的时间间隔调用函数. 当单个内核对象变为已通知状态时调用函数. 当异步IO请求完成时调用函数. 为了完成这些操作,线程池由4个独立的部分组成.下面shix

回炉重造之重读Windows核心编程-014-虚拟内存

14.1 系统信息 GetSystemInfo函数用户检索与主机相关的值,只需要传递SYSTEM_INFO结构体的地址即可. typedef struct _SYSTEM_INFO{ union{ DWORD dwOemId; struct { WORD wProcessorArchiteture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplica

【windows核心编程】线程局部存储TLS

线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来使用动态TLS, 这些函数实际上最为DLL所使用. 系统中的每个进程都有一组 正在使用标志(in-use flag), 每个标志可被设置为FREE 或者 INUSE, 表示该TLS元素是否正在使用. 微软平台保证至少有TLS_MINUMUM_AVALIABLE个标志位可供使用, TLS_MINUMU

【转】《windows核心编程》读书笔记

这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLastError可能用于描述成功的原因(CreatEvent)

【windows核心编程】DLL相关(3)

DLL重定向 因为DLL的搜索路径有先后次序,假设有这样的场景:App1.exe使用MyDll1.0.dll, App2.exe使用MyDll2.0.dll, MyDll1.0 和 MyDll2.0是同一个DLL的两个版本,1.0为旧版本,2.0为新版本. 而如果MyDll2.0.dll的存放路径的优先次序比较靠前时,那么App1.exe就会去加载MyDll2.0.dll,这就可能引发 DLL地狱问题,因此DLL重定向可解决这个问题. 加载程序总是先检查应用程序目录,我们所要做的就是如下: ①在

windows核心编程 DLL技术 【转】

注:本文章转载于网络,源地址为:http://blog.csdn.net/ithzhang/article/details/7051558 本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入,此种方式被称为隐式链接. 第二种方式是在程序运行时,通过调用API显式的载入所需要的DLL,并显式的链接所想要链接的符号.换句话说,程序在运行时,其中的一个线程