《ASP.NET本质论》 线程基础

来源:互联网 发布:java 线程挂起 编辑:程序博客网 时间:2024/05/18 11:28

       线程基础

        在单CPU的情况下,显然计算机同时只能执行一个程序,早期的DOS操作系统就是单任务操作系统,在那个年代,我们在运行一个程序的时候,就不能再运行第二个程序,必须退出当前的程序之后,才能运行另外的程序。到了Windows 3.1的时代,开始采用称为协同多任务的机制,实际上,Windows运行的多个程序并没有真正的同时运行,每个程序都要在适当的时候释放CPU的控制权,以便其他的程序得到执行的机会,这种机制称为协同。此时,只要一个程序死掉,那么,用户除了重新启动系统将没有其他的选择,重要的是,所有在内存中的数据也将同时丢失。

        操作系统的专家们显然意识到这个问题,从Windows NT 开始,开始采用抢先式多任务系统,每一个运行的程序都分配在一个独立的进程(Process)中,一个进程实际上是一个数据结构,描述运行这个程序所需资源的信息,例如内存或者堆栈的使用情况。线程(Thread)是包含在进程中的一种资源,本质上说,线程也是一个特殊的数据结构,用于描述程序执行的状态信息,例如寄存器的状态等。从使用的角度来看,线程就是一个虚拟的CPU,这样,从逻辑上我们可以认为程序运行在一个线程上。

       当然,对于单CPT的机器来说,实际上同时还是只能运行一个程序,操作系统负责系统中哦线程在物理CPU上的切换管理。通常情况下,操作系统通过分配时间片的方式,调度系统中的各个线程,使得看起来似乎在同时运行多个程序。当时间片到期的时候,操作系统将剥夺线程的运行权,以便将CPU调度给其他的线程。这样一个程序死掉,将不会影响到其他的程序,这种调度方式称为抢先式机制。对于多CPU哦系统,操作系统可以将线程分配到不同的CPU,更好地利用多CPU带来的好处。


线程

        通过上面的分析可以看到,操作系统通过线程对程序的执行进行管理。
        当操作系统运行一个程序的时候,首先,操作系统将为这个准备运行的程序分配一个进程,以管理这个程序所需要的各种资源。在这些资源之中,会包含一个称为主线程的线程数据结构,用来管理这个程序的执行状态。
         在Windows操作系统下,线程这个数据结构将会包含以下内容:
              ×线程的核心对象,其中主要包含线程当前的寄存器状态,当操作系统调度这个线程开始运行的时候,寄存器的状态将被加载到CPU中,重新构建线程的执行环境,当线程被调度出来的时候,最后的寄存器状态被重新保存到这里,以备下一次执行的时候使用。

            ×线程环境块(Thread environment block,TEB),是一块用户模式下的内存,包含线程的异常处理链的头部。另外,线程的局部存储数据(Thread Local storage
 Data)也存在这里。

           ×用户模式的堆栈,用户程序的局部变量和参数传递所使用的堆栈,默认情况下Windows将会分配1M的空间用户用户模式的堆栈。

           ×内核模式堆栈,用于访问操作系统时使用哦堆栈。

         在抢先式多任务的环境下,在一个特定的时间,CPU将一个线程调度进CPU中执行,这个线程最多将会运行一个时间片的时间长度,当时间片到期后,操作系统将这个线程调度出CPU,将另外一个线程调度进CPU,我们通常称这种操作为上下文切换。在每一次的上下文切换时,Winddows将执行下面的步骤:
1)将当前的CPU寄存器的值保存到当前运行的线程数据结构中,即其中的线程核心对象中。
2)选择下一个准备运行的线程,如果这个线程处于不同的进程中,那么,还必须首先切换虚拟地址空间。
3)加载准备运行线程的CPU寄存器状态到CPU中。

         公共语言运行时CLR(Common) Language Runtime)是 .NET 程序运行的环境,它负责资源管理,并保证应用和底层操作系统之间必要的分离。
在 .NET环境下,CLR中的线程需要通过操作系统的线程完成实际的处理工作,目前情况下,.NET 直接将CLR中的线程映射到操作系统的线程进度处理和调度,所以我们每创建一个线程将会消耗1M以上的内存空间。但是,为了CLR中的线程并不一定与操作系统中的线程完全对应。通过创建CLR环境下的逻辑线程,我们可能创建更加节省资源的线程,使的大量的CLR线程可以工作在少量的操作系统线程之上。

         在线程的生命的周期中,可以处于多个不同的状态下,刚刚创建的线程处于已经准备好运行,但是还没有运行的状态,我们通常称为(Ready)准备状态。在操作系统的调度之下,这个线程可以进入运行状态,即Running(运行)状态。运行状态的线程可能因为时间片的关系被操作系统切换出CPU,称为Suspended(暂停运行)状态,也可能在时间片还没有用完的情况下,因为等待其他的任务,而转换到Blocked(阻塞)状态。在阻塞状态下的线程,随时可以因为在此的调度重新进入运行状态。线程还可以通过Sleep方法进入Sleep(睡眠)状态,当睡眠时间到期之后,可以在此被调用运行。

        处于运行状态的线程还可能被主动终止执行,直接结束;也可能因为任务已经完成,被操作系统正常结束。

线程的状态转换关系如图


自定义线程


        对于一个运行的程序来说,主线程将有操作系统直接创建并调度,但是,对于许多程序来说,仅仅只有一个线程显然是不够的。
        例如,对于图形用户界面(GUI)来说,绝大多数的用户都是单线程的,因为多线程的用户界面将会带来极大的效率问题。对于Windows的GUI程序来说,消息循环工作在一个线程之上,或者说,整个窗口的输入和蔬菜都是工作在一个线程之上的,这样的话,如果我们在某个菜单的处理中出现了一个耗费很长时间的工作,比如,调用一个计算pi的前100万位的,那么,在这个方法完成之前,我们的用户界面将不会有任何响应,因为计算过程占用了线程,窗口没有机会获得用户的操作,也没有机会更新窗口的现实。
        那么,上面的问题如何解决呢?
        答案是创建自定义的线程来完成计算任务,而界面的线程不要被计算任务所占用,这样,我们的一个线程也将拥有多个线程,这种程序也就是所谓的多线程程序,而这些自定义的线程也被称为自由线程。
        当然,这时就会出啊先线程的调度、线程的同步等线程管理的问题。



前台线程和后台线程

        对于ixiancheng我们还可以分为两类:前台线程和后台线程。
        前台线程能够保持程序的运行,当一个进程中所有的前台线程结束的时候,操作系统将立即结束这个程序进程。注意,启动程序时所创建的主线程一定是前台线程。
         后台线程不能保证程序的存活,这意味着当后台线程还没有完全完成的时候,程序也可能已经结束。当前台线程全部结束的时候,所有的后台线程都将被终止,而且不会有异常抛出。
         如果希望线程能够可靠完成,应该将这个线程设置为前台线程。对于不关键的任务,可以使用后台线程完成。在 .NET 环境下,从非托管代码进入托管执行环境的所有线程都被标记为后台线程。通过创建并启动新的Thread对象而生成的所有线程都默认为前台线程。
         线程对象IsBackground属性是一个可读写的属性,可以用来设置线程是前台还是后台线程。需要注意的是,必须在线程启动之前进行设置,线程启动之后就不能设置了。
         如果使用一个线程监听程序的活动(例如套接字连接),可以将其IsBackground属性设置为true,这样这个监控线程就不会阻塞程序的终止。


工作者线程和I/O线程

        对于线程所执行的任务来说,可以将线程任务分为两种类型:工作者(Worker)线程 和 I/O线程。
工作者线程用来完成计算密集的任务,在任务的执行过程中,需要CPU不间断地处理,所以,在工作者线程的执行过程中,CPU和线程的资源是充分利用的。
        I/O线程典型的情况是用来完成输入和输出工作,在这种情况下,计算机需要通过I/O设备完成输出和输入任务。在处理过程中,操作系统通过计算机的硬件设备完成实际的输入和输出工作,CPU可以不必完全参与到输入和输出的处理过程中,CPU仅仅需要在任务开始的时候将任务的参数传递给设备,然后启动硬件设备即可。等到任务完成的时候,CPU收到一个通知,一般来说,是一个硬件的中断信号,此时,CPU继续后继的处理工作。
        从上面的描述可以看到,在处理的过程中,CPU是不必完全参与处理过程的,如果在运行的线程不交出CPU的控制权,那么线程也只能处于等待状态,在任务完成后才会有事可做,此时,线程所占用的空间还将被使用,但是并没有CPU在使用这个线程,可能出现线程资源浪费的问题。
         如果我们的程序是一个网络服务程序,针对每一个网络连接都使用一个线程进行管理,那么,此时将会出现大量哦线程都在等待网络通信,随着网络连接的不断增加,处于等待状态哦线程将会很快消耗进所有的内存资源。
        面对这些问题可以考虑通过线程池来解决。



线程池

       在前面的分析中,我们任务线程是一个昂贵的资源,仅仅从内存的角度来说,每个线程就将占用1M以上的内存,而且,初始化内存中的数据结构,包括在消耗线程时的处理,都更加显得线程是一个昂贵的资源。
        针对这种情况,我们可以考虑使用少量的线程来管理大量的网络连接,比如说,在启动输入输出处理之后,只知用一个线程监控网络通信的状况,在这种情况下,需要进行网络通信的线程在启动通信开始之后,就已经可以结束了,也就是说,可以被系统回收了。在通信的传输阶段,由于不需要CPU参与,可以没有线程介入。监控线程将负责在信息到达之后,重新启动一个计算密集的线程完成本地的处理工作。这样带来的好处就是将没有线程处于等待状态来消耗有效的内存资源。
        所以,对于I/O线程来说,可以将输入输出的操作分为三个步骤:启动、实际输入输出、处理结果。由于实际的输入输出可由硬件完成,并不需要CPU的参与,而启动和处理结果也并不需要必须在同一个线程上进行,为了提高线程的利用率,可以将输入输入的操作分为两步来进行,以便充分利用线程资源。一般来说,在.NET中启动步骤的方法名称以Begin作为前缀,而处理结果的方法以End作为前缀,这两个方法可以运行在不同的线程上。
        为了减少创建线程、销毁线程所带来的效率损失,同时也为了能够节约宝贵的内存,可以考虑创建一个线程池,提供线程的工厂服务,这样,就没有必要总是创建新的线程,而是当需要线程的时候从线程池取出一个线程,当不再使用这个线程的时候,将这个线程归还给线程池,以方便后继的使用。
        如果需要大量的线程完成处理工作,还可以考虑创建一个线程的消费队列,将需要线程处理的操作根据先入先出的顺序排在一个队列中,而线程池中仅仅需要少量的线程就可以主次完成队列的操作。线程池的处理过程如图:

原创粉丝点击