NET基础拾遗(5)多线程开发基础 多线程编程的基本概念下面的一些基本概念可能和.NET的联系并不大但对于掌握.NET中的多线程开发来说却十分重要。我们在开始尝试多线程开发前应该对这些基础知识有所掌握并且能够在操作系统层面理解多线程的运行方式。1.1 操作系统层面的进程和线程1进程进程代表了操作系统上运行着的一个应用程序。进程拥有自己的程序块拥有独占的资源和数据并且可以被操作系统调度。But即使是同一个应用程序当被强制启动多次时也会被安放到不同的进程之中单独运行。直观地理解进程最好的方式就是通过进程管理器浏览其中每条记录就代表了一个活动着的进程2线程线程有时候也被称为轻量级进程它的概念和进程十分相似是一个可以被调度的单元并且维护自己的堆栈和上下文环境。线程是附属于进程的一个进程可以包含1个或多个线程并且同一进程内的多个线程共享一块内存块和资源。由此看来一个线程是一个操作系统可调度的基本单元但是它的调度受限于该线程所属的进程也就是说操作系统首先决定执行下一个执行的进程进而才会调度该进程内的线程。一个线程的基本生命周期如下图所示3进程和线程的区别最大的区别在于隔离性每个进程都会被单独隔离进程拥有自己的内存、资源和运行数据一个进程的崩溃不会影响到其他进程因此进程间的交互也相对困难而同一进程内的所有线程则共享内存和资源并且一个线程可以访问和结束同一进程内的其他线程。1.2 多线程程序在操作系统中是并行执行的吗1线程的调度在计算机系统发展的早期操作系统层面不存在并行的概念所有的应用程序都在排队等候一个单线程的队列之中每个程序都必须等到前面的程序都安全执行完毕之后才能获得执行的权利一个小小的错误将会导致操作系统上的所有程序的阻塞。在后来的操作系统中逐渐产生了分时和进程、线程的概念。多个线程由操作系统进行调度控制决定何时运行哪个线程。所谓线程调度是指操作系统决定如何安排线程执行顺序的算法。按常规分类线程调度可以分为以下两种①抢占式调度抢占式调度是指每个线程都只有极少的运行时间在Windows NT内核模式下这个时间不会超过20ms而当时间片用完时该线程就会被强制暂停保存上下文并把运行权利交给下一个线程。这样调度的结果就是所有的线程都在被不停地快速切换运行使得用户感觉所有的线程都在并行运行。②非抢占式调度非抢占式调度是指某个线程在运行时不会被操作系统强制暂停它可以持续地运行直到运行告一段落并主动交出运行权。在这样的调度方式之下线程的运行就是单队列的并且可能产生恶意程序长期霸占运行权的情况。PS现在很多的操作系统包括Windows在内都同时采用了抢占式和非抢占式模式。对于那些优先级较高的线程OS采用非抢占式来给予充分的时间运行而对于普通的线程则采用抢占式模式来快速地切换执行。2线程的并行问题在单核单CPU的硬件架构上线程的并行运行完全是用户的主观体验。事实上在任一时刻只可能存在一个处于运行状态的线程。但在多CPU或多核的架构上情况则略有不同。多CPU多核的架构则允许系统完全并行地运行两个或多个无其他资源争用的线程理论上这样的架构可以使运行性能整数倍地提高。PS微软公司曾经提出超线程技术简单说来这是一种逻辑上模拟多CPU的技术但实际上它们却共享物理处理器和缓存超线程对性能的提高相当有限。1.3 神马是纤程1纤程的概念纤程是微软公司在Windows上提出的一个概念其设计目的是用来方便地移植其他操作系统上的应用程序。一个线程可以拥有0个或多个纤程一个纤程可以视为一个轻量级的线程它拥有自己的栈和上下文状态。But纤程的调度是由程序员编码控制的当一个纤程所在线程得到运行时程序员需要手动地决定运行哪一个纤程。PS事实上Windows操作系统内核是不知道纤程的存在的它只负责调度所有的线程而纤程之所以成为操作系统的概念是因为Windows提供了关于线程操作的Win32函数能够方便地帮助程序员进行线程编程。2纤程和线程的区别纤程和线程最大的区别在于线程的调度受操作系统的管理程序员无法进行完全干涉。但纤程却完全受控于程序员本身允许程序员对多任务进行自定义的调度和控制因此纤程带给程序员很大的灵活性。下图展示了进程、线程以及纤程三者之间的关系3纤程在.NET中的地位需要谨记是的一点是.NET运行框架没有做出关于线程真实性的保证也就是说我们在.NET程序中新建的线程并不一定是操作系统层面上产生的一个真正线程。在.NET框架寄宿的情况下一个程序中的线程很可能对应某个纤程。PS所谓CLR寄宿就是指CLR运行在某个应用程序而非操作系统内。常见的寄宿例子是微软公司的SQL Server 2005。二、.NET中的多线程编程.NET为多线程编程提供了丰富的类型和机制程序员需要做的就是掌握这些类型和机制的使用方法和运行原理。2.1 如何在.NET程序中手动控制多个线程.NET中提供了多种实现多线程程序的方法但最直接且灵活性最大的莫过于主动创建、运行、结束所有线程。1第一个多线程程序.NET提供了非常直接的控制线程类型的类型System.Threading.Thread类。使用该类型可以直观地创建、控制和结束线程。下面是一个简单的多线程程序View Code在主线程中该代码创建了10个新的线程这个10个线程的工作互不干扰宏观上来看它们应该是并行运行的执行的结果也证实了这一点PS这里再次强调一点当new了一个Thread类型对象并不意味着生成了一个线程事实上线程的生成是在调用Thread的Start方法的时候。另外在之前的介绍中这里的线程并不一定是操作系统层面上产生的一个真正线程2控制线程的状态很多时候我们需要主动关心线程当前所处的状态。在任意时刻.NET中的线程都会处于如下图所示的几个状态中的某一个状态上该图也直观地展示了一个线程可能经过的状态转换过程该图并没有列出所有的状态转换途径/原因下面的示例代码则展示了我们如何手动地查看和控制一个线程的状态View Code上述代码的执行结果如下图所示PS为了演示方便上述代码刻意地使线程处于各个状态并打印出来。在.NET Framework 4.0 及之后的版本中已经不再鼓励使用线程的挂起状态以及Suspend和Resume方法了。2.2 如何使用.NET中的线程池1.NET中的线程池是神马我们都知道线程的创建和销毁需要很大的性能开销在Windows NT内核的操作系统中每个进程都会包含一个线程池。而在.NET中呢也有自己的线程池它是由CLR负责管理的。线程池相当于一个缓存的概念在该池中已经存在了一些没有被销毁的线程而当应用程序需要一个新的线程时就可以从线程池中直接获取一个已经存在的线程。相对应的当一个线程被使用完毕后并不会立刻被销毁而是放入线程池中等待下一次使用。.NET中的线程池由CLR管理管理的策略是灵活可变的因此线程池中的线程数量也是可变的使用者只需向线程池提交需求即可下图则直观地展示了CLR是如何处理线程池需求的PS线程池中运行的线程均为后台线程即线程的 IsBackground 属性被设为true所谓的后台线程是指这些线程的运行不会阻碍应用程序的结束。相反的应用程序的结束则必须等待所有前台线程结束后才能退出。2在.NET中使用线程池在.NET中通过 System.Threading.ThreadPool 类型来提供关于线程池的操作ThreadPool 类型提供了几个静态方法来允许使用者插入一个工作线程的需求。常用的有以下三个静态方法① static bool QueueUserWorkItem(WaitCallback callback)② static bool QueueUserWorkItem(WaitCallback callback, Object state)③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)有了这几个方法我们只需要将线程要处理的方法作为参数传入上述方法即可随后的工作都由CLR的线程池管理程序来完成。其中WaitCallback 是一个委托类型该委托方法接受一个Object类型的参数并且没有返回值。下面的代码展示了如何使用线程池来编写多线程的程序View Code上述代码执行后如果不输入任何字符那么会得到如下图所示的执行结果PS事实上UnsafeQueueWorkItem方法实现了完全相同的功能二者的差别在于UnsafeQueueWorkItem方法不会将调用线程的堆栈传递给辅助线程这就意味着主线程的权限限制不会传递给辅助线程。UnsafeQueueWorkItem由于不进行这样的传递因此会得到更高的运行效率但是潜在地提升了辅助线程的权限也就有可能会成为一个潜在的安全漏洞。2.3 如何查看和设置线程池的上下限线程池的线程数是有限制的通常情况下我们无需修改默认的配置。但在一些场合我们可能需要了解线程池的上下限和剩余的线程数。线程池作为一个缓冲池有着其上下限。在通常情况下当线程池中的线程数小于线程池设置的下限时线程池会设法创建新的线程而当线程池中的线程数大于线程池设置的上限时线程池将销毁多余的线程。PS在.NET Framework 4.0中每个CPU默认的工作者线程数量最大值为250个最小值为2个。而IO线程的默认最大值为1000个最小值为2个。在.NET中通过 ThreadPool 类型提供的5个静态方法可以获取和设置线程池的上限和下限同时它还额外地提供了一个方法来让程序员获知当前可用的线程数量下面是这五个方法的签名① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)② static void GetMinThreads(out int workerThreads, out int completionPortThreads)③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)④ static bool SetMinThreads(int workerThreads, int completionPortThreads)⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)下面的代码示例演示了如何查询线程池的上下限阈值和可用线程数量View Code该实例的执行结果如下图所示PS上面代码示例在不同的计算机上运行可能会得到不同的结果线程池中的可用数码不会再初始时达到最大值事实上CLR会尝试以一定的时间间隔来逐一地创建新线程但这个时间间隔非常短。