.NET中的线程

      早在2001年的时候,IBM就推出了在一个CPU上集成两个运算核心的服务器,但桌面双核的到来却是2005年4月18号,Intel发布了其历史上第一颗双核CPU-奔腾至尊版840,从此,千千万万的普通用户也进入了多核时代。随着多核的到来,以前随CPU频率的提高而带来的性能提升已经成为历史,软件开发人员必须去面对多核编程,而其中绕不过的一个中心就是多线程。本文围绕Windows平台展开,讨论.NET所支持的线程。

一、历史回顾

      众所周知,Windows早期是不支持多线程的,一个进程贯穿整个系统,同时,它也是一个线程。这使得同一时间内只能运行一个任务,如果这个任务运行时间过长,那么用户只有等待任务执行完毕。遗憾的是,如果程序中不小心出现了死循环,那么用户就只有按重启键重启操作系统了,这会导致某些重要的用户数据丢失,用户体验也非常差。微软为了解决这个问题,引入了进程的概念,进程是应用程序运行的实例,每个进程都有独立的内存空间,进程间不能相互访问。这种机制的好处是如果一个进程出现问题,如崩溃,不会影响到其它进程的正常运行,使Windows更加稳定,安全了。但问题又来了,如果一旦某个进程中的代码出现死循环,那么这个进程会占用全部CPU资源,而致使其它程序无法获得CPU资源,由于进程间的隔离性,其它应用程序也无法中止它。因此,微软又引入了线程机制,线程允许其它应用程序(例如任务管理器)强制中止某个线程的执行。值得一提的是,发布于1993年的Windows NT 3.1(首款Windows 32位操作系统)标志着Windows操作系统进入了多线程时代。

二、Windows线程详解

     线程是CPU的执行单元,一个进程可以包含一个或多个线程,同一进程间的多个线程共享进程资源。操作系统通过线程之间的切换执行实现了多任务的并发执行。在Windows中,创建一个线程的代价是很大的,虽然现在硬件的速度对于创建一个线程来说所用时间非常小,但我们了解这些细节对于我们是非常有用的。

    1、线程的开销

    ● 线程内核对象 当线程被创建时,操作系统要分配与初始化的数据结构。包括一系列描述线程的属性以及线程上下文。

    ● 线程环境块 是用户模式下分配和初始化的内存块,它包含线程异常处理链的头信息。每当线程中的代码进入try语句块时,就会插入一个标志在当前线程异常处理链的头信息中,当退出try语句块时,这个标志就会被移除。

    ● 用户模式栈 用于存储局部变量、传递给方法的参数,以及方法返回值,即我们通常所说的线程栈。

    ●  内核模式栈 用于存储应用程序传递参数给内核方法。

    ●  DLL线程附加和线程卸载通知标志 用于DLL的加载与卸载的相关工作。

    2、线程上下文切换

      不管是以前的单核CPU还是如今的多核CPU,至始至终,CPU是有限的,是众多程序必用的公共资源。理想的情况下,不用线程切换的程序的性能是最高的,但这又成了单线程了,不是我们想要的结果。所以,不管怎样,线程切换是必不可少的开销。由于CPU在某一时刻只能执行一个任务,因此,操作系统把CPU的时间分成极为细小的时间片(大约30毫秒),在某个时间片内,执行某个任务,时间片结束后,操作系统就会把当前正在执行的线程挂起,切换到下一个要执行的线程并执行,这就叫线程上下文切换。线程上下文切换的步骤是:

    (1)、保存CPU寄存器的值到当前线程内核对象中的线程上下文中

    (2)、选择并调度一个等待线程集合中的线程,如果这个线程与上一个线程不在同一个进程中,那么线程在正式开始前切换到当前进程的虚拟地址空间。

    (3)、加载线程上下文数据到CPU寄存器并开始执行

     经过这三步操作,操作系统就把一个线程切换到另一个线程,线程之间的切换就如此反复地进行着。

    3、线程的优先级

      Windows是一个抢占式的多线程操作系统,而这一切,由线程的优先级来决定。当Windows启动后,会有众多的线程等待执行,如此众多的线程到底先执行谁呢?这是一个问题,于是就有了线程优先级,当然是执行优先级高的线程了。线程的优先级不仅决定着线程的执行先后顺序,而且也决定着线程分得CPU时间片的多少。例如,当前有个优先级为3的线程A在执行,此时,半路杀出个优先级为5的线程B,由于Windows的抢占性,这时,不管线程A的CPU时间片还剩多少,也不管线程A执行到哪步了,彪悍的操作系统立即挂起线程A,切换到线程B执行。等线程B时间片结束后,操作系统会分一小片时间片给线程A执行,然后再分多一些的时间片给线程B执行,如此往复,直到两个线程执行完毕。

     就如线程的优先级一样,进程也有优先级,Windows把进程分为六个优先级,Idle, Below Normal, Normal, Above Normal, High, 以及Realtime,在.NET类库中,我们可以从System.Diagnostics.ProcessPriorityClass枚举中看到关于进程优先级的定义。Normal是默认的进程优先级,值得注意的是,我们要慎用Realtime优先级,因为这个优先级太高了,以致会干扰到操作系统的运行。

     Windows把线程分为32个优先级,从0到31。为了简化我们的记忆,它们被定义为Idle, Lowest, Below Normal, Normal, Above Normal,Highest, 以及Time-Critical。在.NET类库中,我们可以在System.Threading.ThreadPriority枚举中看到相关定义。为了方便说明进程优先级是如何影响线程优先级的,请看下表:

threadPriority

     我们可以通过设置线程的Priority属性来指定线程的优先级,值得注意的是,在.NET中,一旦指定了进程的优先级以后,就不能更改,因为,更改进程的优先级会涉及到大量线程的优先级也会做出更改,这样会影响到程序的运行。

三、.NET中的线程

     我们都知道,在.NET中,通过System.Threading.Thread类来创建一个线程,叫做托管线程。而实际发生的情况却不是我们想象中的那样,.NET并没有实际创建线程,而是利用Windows的线程处理能力。System.Threading.Thread实例只是包装了Win32线程。甚至有时候,.NET线程并没与Win32线程一一对应。

     1、前台线程VS后台线程

     当我们创建一个新线程时,通过指定IsBackground的值来设置这个线程是前台线程还是后台线程。简单的说,前台线程会一直运行直到任务完成或者强制中止,即使应用程序结束也仍会运行;而后台线程就不会这样,当程序结束,不管后台线程执行到什么状态,都会随应用程序的结束而中止。

     2、线程池

     通过第二节的描述我们知道,创建一个新线程开销相当的大,如果反复频繁的创建销毁线程,会影响程序性能,所以微软在.NET中加入了线程池。简单地说,线程池是一个线程集合,当我们把要执行的任务交给线程池去处理后,我们并不关心这个任务由哪个线程来执行,也不关心执行完任务后要不要立即销毁线程。这一切,都由线程池来打理。

      3、并行库

     为了减小并行代码的开发难度,微软在.NET 4.0中提供了并行库。所谓并行,就是通过多个线程同时执行一个任务,这样,分配的CPU时间片就更多,任务执行得也就越快。.NET 4.0通过System.Threading.Tasks.Parallel类提供并行处理能力。

四、总结

 

     多核时代,面对着繁重的处理任务,同时也为了使CPU的处理能力发挥出来,我们会创建更多的线程来完成我们的工作。但我们编程的复杂性也随之增加,遇到的问题也会更多,只有深入线程底层,了解线程的更多知识,对我们的工作会带来更多的帮助。

 

     参考文献:《CLR Via C#》,Third Edtion,作者:Jeffrey Richter,P691~719。

你可能感兴趣的:(.net)