《windows核心编程系列》五谈谈线程基础

线程的基础
线程也有两部分组成。一个是线程内核对象。它是一个数据结构,操作系统用它来管理线程以及用它来存储线程的一些统计信息。另一个是线程栈,用于维护线程执行时所需的所有函数参数和局部变量。位于同一个进程的线程共享进程的地址空间且它们共享进程句柄表。因为句柄表是针对进程的。进程需要很多的系统资源,而线程仅仅需要一个线程内核对象和线程栈就可以了,因此线程比进程的开销要小得多。采用多线程来处理问题也是理所当然的了。
采用多线程可以提高程序的执行效率,但是多线程也存在很多问题。在尝试使用多线程时如果处理不当还可能会引入新的问题。如同步问题。

每个线程都需要一个入口点函数。这是线程执行的起点。主线程的入口点函数是_tmain或_tWinmain。如果在进程中创建新线程必须提供自己的入口点函数。

DWORD ThreadProc(PVOID pvParam)  
  
{  
  
   int temp=(int)pvParam;  
  
    ....  
  
   return 0;  
  
} 

线程函数可以是任何我们希望它执行的任务,最终线程函数会终止并返回。
线程函数只有一个参数,其意义可由我们定义。可以为其传递一个值,也可以将其作为某个数据结构的指针。这需要在线程函数内部做类型转换。
线程函数必须返回一个值,它的值传递给ExitThread,作为线程的退出代码。
线程函数尽可能地使用局部变量或函数参数,它们是在线程栈上创建的。不太可能被其他线程破坏。使用静态变量或全局变量时其他线程可以访问这些变量,这会导致同步和互斥问题。

每个线程都有自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一次执行时,线程的CPU寄存器的状态。线程的CPU寄存器全部保存在一个CONTEXT结构。CONTEXT结构本身保存在线程内核对象中。线程始终在进程的上下文中运行。 当线程内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为 pfnStartAddr 在线程堆栈中的地址,而指令指针寄存器被设为 RtlUserThreadStart 函数:

线程的组成:

1.一个线程内核对象,操作系统用它来管理线程。内核对象中还存储了线程的各种统计信息,包括挂起计数、退出代码等,以便于系统对线程的管理。内核对象中有一个CONTEXT结构,这个结构中存储了线程上一次执行的时候CPU寄存器的状态。

2.一个线程栈,用于维护线程执行时所需的所有函数参量和局部变量。

线程的运行:

在解释线程的运行机制之前,首先回顾一下过去单线程程序的运行机制:

1.程序是一条指令接着一条指令顺序执行的,回忆下之前单片机课上学习的汇编语言。

2.CPU现在正在执行的指令地址被存储在IP寄存器里面,这条指令执行完之后,IP就会自动增加,这样CPU下次就会接着执行下一条指令了。这个可以通过DEBUG指令在命令行里查看一下,具体还是要回忆下之前《汇编语言》这本书里面学到的东西。

3.为了便于管理,在汇编代码里经常把程序分成几个"段",包括"代码段",“栈段”、“数据段”。这样把所有代码指令都放到同一个地方,临时变量之类的放到另外一个地方,管理起来容易不少,后来栈的设计被CPU广泛支持,有一个寄存器SP专门就用来存储栈顶指针。具体的还是要回忆下《汇编语言》里学的东西。

下面解释Windows下多线程程序的运行机制:

1.如果有两个CPU的话,运行两个线程,事情当然好办许多,两个CPU各有自己的IP和SP寄存器,代码段应该是共享的、只需要分别建立两个"栈段",两个CPU各自访问自己的临时数据即可,还是原来的指令顺序执行,互相并不干扰。

2.而对于一个CPU运行两个线程的情况,事情就比较麻烦,毕竟需要用一个CPU模拟出两个CPU的效果。想要达到这种效果,最直观的办法当然是有一个变量把每个线程当前执行的状况——也就是当前CPU的寄存器值存储下来。每次从当前线程切换到另一个线程的时候,都先把当前线程的寄存器状况存下来,然后把另一个线程之前已经存好的寄存器状况载入到CPU里去执行。为了两个线程之间互不干扰,它们也应该各自有一个"栈段",来存储自己执行过程中的临时变量什么的。

3.在Windows系统里当前CPU寄存器的值以CONTEXT结构的形式被存储在线程内核对象里面。《Windows核心编程》一书中有CONTEXT结构定义,可以看到其中确实有ESP,EIP寄存器。

4.线程内核对象之中还记录了挂起计数、使用计数等信息。Windows用这些信息来管理线程。

5.Windows中多线程程序的运行机制简述如下:

每隔大概20ms,Windows都会查看所有当前存在的线程内核对象,在这些对象中,只有一部分被认为是可调度的(可根据线程内核对象中记录的信息来判断)。Windows根据某种算法在可调度的线程内核对象中选一个,并将上次保存在线程上下文(CONTEXT)中的值载入CPU寄存器。这一操作被称为上下文切换。线程上下文中的值载入CPU寄存器之后,也就把上一次线程的运行状态装载到了CPU里面,线程所执行的指令也就可以被运行了。

现在,应该可以理解为什么线程的组成只是线程内核对象和线程栈这么简单,也理解了线程之间切换时为什么线程中的指令能够接着上次没执行完的地方继续执行,另外,还理解了通过线程内核对象,Windows才能对线程进行有效的管理——内核对象里记录了线程那么多的信息,而线程栈仅仅是为线程执行提供临时空间而已。

线程的创建:

Windows中负责线程创建的API是CreateThread,虽然_beginthreadex等也可以创建线程,但只不过是调用了CreateThread而已。下面介绍CreateThread是如何创建一个线程的:

1.一个线程内核对象被创建出来,需要注意的是它的挂起计数被设定为1,这个时候线程还没创建完毕,但是线程内核对象已经创建出来了,为了使系统轮询的时候线程不被执行到,挂起计数不能被设定为0。另外这个时候内核对象的CONTEXT结构里的值还没被设定。

2.线程栈被创建出来,其中被传进了两个变量,一个是线程函数的地址,一个是线程函数的参数。这两个变量在之后函数调用的时候会用到。

3.内核对象的CONTEXT结构里,IP寄存器的值被设定为RtlUserThreadStart函数的地址,而SP寄存器的值指向了之前创建好的线程栈,通过SP就可以找到之前传到栈里的两个变量了。

4.经过以上操作,线程就被初始化好了,接着启动便是,把挂起计数递减到0。系统轮询线程内核对象的时候,发现此线程可以执行了,就执行CONTEXT寄存器里存储的指令,在这里也就是执行RtlUserThreadStart函数,RtlUserThreadStart函数有两个参数,这两个参数就是由SP来指定的那两个变量——线程函数地址和线程函数参数。

5.RtlUserThreadStart会在内部调用线程函数,并把参数传到线程函数里,这样线程函数就被执行起来了。

6.当线程函数返回的时候,RtlUserThreadStart会为它调用ExitThread来结束线程。

接下来介绍线程的创建,与其他内核对象一样,它也有对应的Create*函数。

//创建线程

HANDLE CreateThread(
    PSECURITY_ATTRIBUTES    psa,            //线程安全属性
    DWORD                   cbStackSize,    // 线程堆栈大小,默认是 1MB(Itanium芯片上,默认大小是4MB)
    PTHREAD_START_ROUTINE   pfnStartAddr,   // 线程函数地址
    PVOID                   pvParam,        // 传入线程的参数
    DWORD                   dwCreateFlags,  // 控制线程的标志: CREATE_SUSPENDED: 创建之后挂起; 0: 创建之后立即执行
    PDWORD                  pdwThreadID);   // 返回值:线程ID
    );

返回值:
若线程创建成功,则返回线程句柄。

线程的销毁:

销毁一个线程有多种方式,方式之间各有区别。下面逐一介绍:

1.线程函数退出:

这种方式是最正常和最正确的方式。因为是从函数里正常退出的,在线程执行过程中创建的临时对象都能够调用它们的析构函数而死去。退出之后会执行ExitThread函数来销毁线程所占有的系统资源——正如之前的介绍,线程函数其实是退出到了RtlUserThreadStart函数那里,有RtlUserThreadStart函数来负责调用ExitThread。

2.调用ExitThread:

这种方式不大好的地方在于线程执行过程中创建的临时对象没办法调用它们自己的析构函数,根据MSDN的说法,ExitThread将会直接销毁本线程所拥有的线程栈——当然也包括线程栈中存储的临时对象。在实际情况中我们经常在构造函数里new出一块内存来使用,析构的时候再delete它,如果对这种情况调用了ExitThread,析构函数无法被调用,new出来的那块内存也就没办法释放了,造成了内存泄漏。所以不推荐这种线程退出的方式。

ExitThread还会执行一些系统上的操作,比如内核对象使用计数递减,关闭线程中使用过的任何I/O端口,与使用过的DLL脱离关系等等。

3.调用TerminateThread:

TerminateThread相对与ExitThread来说更不推荐使用,除非在极其了解要终结的线程代码如何构建的情况下,TerminateThread才有可能被安全地使用,它所造成的危害在MSDN上已经有比较清楚的说明。比如堆没法释放,关键代码段没机会关闭等等。

并且TerminateThread不会销毁线程的线程栈,但他会把线程内核对象的使用计数递减——这意味着线程内核对象有可能已经被销毁了,但是线程栈还存在着。Microsoft故意以这种方式来实现TerminateThread。否则,加入其他还在运行的线程要引用被杀死的那个线程的堆栈上的值,就会引起访问违规。让被杀死的线程的堆栈保留在内存中,其他的线程就可以正常运行。

4.包含线程的进程终止时:

进程终止的时候会杀死所有的线程,不知道具体杀死的方法是什么。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值