这几天,花了些时间,浏览了下《C#线程参考手册》,对初学者比较有用。。。
该书可以在我CSDN下载频道获得,请购买原书支持正版(http://lzhdim.download.csdn.net/)。
几年前买过一本Intel的工程师写的《多核程序设计技术》一书,本来想开个专题来对多核程序的设计做介绍的,由于时间问题,该专题改为“并行程序设计”了,但该书的重要内容却没有记录下来,比较遗憾,后续有时间再补吧。(现在叫并行程序的比较多)
其实Intel组织开展过多次并行程序的活动和编程专题,一来推广它的多核CPU,二来对推进并行程序的设计开发做铺垫,毕竟它和微软也是老伙伴了,向来不是我的软件推动你的硬件的发展,要不就是我的硬件更多的系列来支持你的软件更新换代。
其实对于CPU的多核的发展,我觉得是挺慢的。早在多年前,DSP的硬件就已经支持并行处理了,而且有不少的芯片系列,开发板之类的,对于那些应用早就如火如荼的开展了(当时CPU还是单核的,服务器要装几个CPU,即主板上有几个CPU插槽)。而电脑CPU的发展比较缓慢,一个是由于硬件工艺技术上的发展限制(其实也挺快了,Intel一直都是用摩尔定律来进行硬件的升级发展),主要是nm级的火拼吧;一个也是价格上的问题,毕竟新工艺在实验室里研究成功后,还需要一定的时间才能投入到生产中;一个也是前面的CPU系列的更新换代问题,厂商们需要时间来推广和销售他们对应的电脑产品,比如主板,内存之类(产品线的更新是个大问题);还有一个重要的,就是操作系统的支持。操作系统需要根据新的硬件升级,更好的发挥出硬件的能力,更多的榨取硬件的价值。操作系统的价值不仅仅在于配合硬件,更好的提供客户的体验才是最主要的。(现在GPU的发展倒是挺快,抢了CPU的风头。CPU最初的应用就是计算,结果现在倒是大幅度的应用GPU的计算能力,真是对CPU的讽刺。)
墨迹了这么多,转入正题吧。。。
一、说到线程,从硬件CPU开始。早期的CPU技术,单核的,比如超线程技术,它的实质是在逻辑上(不是物理)映射另一个CPU核心,然后共享CPU的缓存,以软件的分配调度方式来模拟多核的应用(硬件底层是需要底层的软件代码来支持,即芯片内部的数据处理代码,其上才是操作系统,而操作系统需要再通过设备驱动程序才能访问该硬件)。这种支持超线程的CPU,在windows任务管理器中,能够看到2个至多个CPU使用记录的显示,但其实质上仍是1/2个硬核(现在的CPU,比如4核,如果支持超线程技术,那么显示为8个CPU记录,其它类推)。Intel刚开始推出超线程CPU技术,貌似挺好,但是早期的硬件设计、驱动以及操作系统的支持问题,Intel曾一度停止该超线程技术的应用。但是到了后来,因为技术成熟了,所以又开始应用该技术到CPU里。还是实际的硬核才是真道理。
CPU不知道什么线程,它只负责处理数据。早期的总线型技术,能够提供的带宽相对比较小,随着硬件的发展,已经限制了CPU处理数据的速度,于是,最新的QPI型技术出来了,带宽增大了,当然,目前只在X58、P55之类的主板上才支持,新技术开始总是贵的。对于CPU来说,它只知道二进制指令(RISC指令集和CISC指令集)和二进制数据,而数据的长度(位数),即32bit、64bit决定了CPU处理数据的大小。芯片级代码的算法,就已经控制和调度哪个空闲的CPU核来处理并行的数据。所以,操作系统只需要调用硬件CPU厂商提供的驱动程序,并控制线程队列的运作即可,实质上做的是中介的应用。
二、这里描述下线程的调度顺序。 用户应用程序 -> 操作系统 -> HAL -> 驱动程序 -> 主板北桥芯片组(P55只有南桥) -> 主板总线 -> CPU核心调度算法 -> CPU指令集 -> CPU缓存 -> CPU核心 。(这个顺序是我对硬件的理解,如果大家有不同的意见,欢迎批评指正)。
我们用C#编写的并行程序,受CLR托管,而并行程序中的线程,受操作系统的管理。.NET框架已经提供了对线程的调用的方法集,考虑了线程的创建,更新,通信,同步,数据锁,异步通信等等问题。所以,除非特别需要,尽量使用框架提供的方法来操作线程,以取得更好的性能和效率以及控制力。
1、理解线程的生命期。
主要是对线程的状态变化进行理解,对于后面理解线程的运行机制和使用代码控制线程提供基础。
上图介绍了C#中线程的操作方法和状态。应该对该图有个印象,后面应用这些方法就简单了。
2、理解线程所处的环境。
要使用C#提供的线程操作功能,必须先搞清楚线程所处的运行环境。下图展示了基本环境。
上图是一个简要的环境描述。其中,CLR运行于操作系统上,而托管的应用程序进程,则运行在CLR的控制下。应用程序域对应于程序集。每个域里面,可能没有线程,也可能会有多个线程。
3、调用线程。
C#中调用线程很简单。Thread t=new Thread(new ThreadStart(Function)); t.Start();即可。这里线程的回收,也是由GC处理。
线程的优先级也同样比较重要,当然也不能随意设置,太多的高优先级的线程将抢占CPU资源,反而会导致操作系统性能下降。线程的同步和线程安全是需要特别注意的地方。如果处理不好,则会导致资源竞争,导致死锁等问题。
线程的同步,.NET框架中提供了几个操作类进行处理和控制。这个需要对各个操作类的应用深入了解,选择性的进行使用,以提供应用程序性能。
比如,对共享资源的锁定及重要代码段的锁定,一般习惯性的用下列代码来实现:
代码1 lock( object )
2 {
3 //do something
4 //deal with object
5 }
这个是常用的方式,在IL中生成的代码,与使用下面的代码类似,在IL上没啥区别。
代码1 Monitor.Enter( object );
2 //do something
3 //deal with object
4 Monitor.Exit();
这里有个小问题,Monitor.Enter( object );该方法会在资源object争用时导致线程等待(从而就会有死锁的可能发生),所以适合于该线程处理的内容为需要等待处理结束的应用。而如果是线程对线程的调度,或者线程监控某个资源的应用下,就得使用bool b=Monitor.TryEnter( object );该方法如果获取不到资源,则b将为false,这样下面就可以根据b来做分支判断是否执行处理数据代码了,否则可以结束该线程,等待下一个新线程对该资源的访问,从而不用等待资源的释放。选择哪个应用取决于实际环境的分析和设计了。
4、线程池技术。
线程池技术在多线程程序的效率上节省了创建新线程的时间,转为对线程资源的调度应用上。当然,线程池也不是万能的。它主要是应用在短暂的线程运行处理上,而不适用于某个处理大且长的应用上。对于线程池的应用,直接使用.NET框架中的ThreadPool操作类即可,其内置的处理方法与操作系统的配合,是一种高效的应用线程池的方案。
如果在特殊场合,需要自己建立线程池的话(或者存储其它与线程类似的对象的对象池),建议尽量使用HashSet<T>泛型类来进行处理,而不要使用数组的方式来进行存储。该书中就是使用了ArrayList数组来进行处理。线程池一般都固定大小,所以会使用数组来进行处理。但是ArrayList也是长度可变的数组。在对存储的内容的处理上,数组也是存储在托管堆上的,但是它的区域是连续的内存区域,这个是它的特点,也是优点。而HashSet使用的是Hash的方式进行的存储,对于存储内容的处理上,对于增减内容的操作,对比固定数组的处理上要高效,因为数组如果删除中间的某个内容,需要循环以将后续的内容来填充至该删除的区域,可能降低了效率。在此不多说了,大家可以写些DEMO来做性能判断。
1、我举个例子。线程池就象是工厂里面的多条生产线。需要生产产品的时候,我就取一条空闲的生产线来进行处理。生产达到任务后就让这条生产线空出来,等待 下一个生产调用。如果没有空闲的生产线,那么我会让该任务等待一下再去处理,或者增加一条生产线来处理任务,实在不行,再根据任务优先级来暂停某条生产 线,优先处理现在需要处理的任务。。。
2、对于线程池中的线程,使用完后不是释放它的资源,而是让它空闲出来。是我这个“增减”没有描述清楚,是一个实现方式的问题,才导致了你的误会,下面说一下。
3、那么,线程池怎么实现呢?
如果使用一个固定长度的数组来实现的话,那么,就需要循环遍历数组来查找空闲可用的线程,在多个请求空闲线程的时候,还需要锁定该线程资源来保证线程安全等等。。。这个是一个实现方式。
另 一个实现方式,就如书中所描述的。使用一个数组来存储已在使用线程,使用另一个数组来存储空闲的线程。请求空闲线程,直接从空闲数组中获取线程,并保存到 已使用数组中。已使用数组中的线程,完成任务后就保存到空闲数组中。这个就是我所说的线程“增减”的问题。。。这个是另一个实现方式。
至于这两种方式,效率和性能的取舍就要看大家怎么应用了。
5、多线程程序的调试
VS中提供了工具,用来对多线程程序的调试提供了便利。具体请看该书的第6章。
上述是说了几点,还是没有将概念讲透,请大家仔细阅读该书。
下面给出些小参考:
1、对于CPU硬件来说,主要的在与其运行的频率高低,决定了它的运行速度。所以,对于一个单核频率为3.0G的CPU,和一个双核2.0G的CPU,在使用单线程的应用程序,或者少量的多线程应用程序来说,由于3.0G的运行速度,那么其将比2.0G的双核CPU运行得快。而如果多线程的应用程序环境下,2.0G双核的CPU不定会比单核3.0G运行得快,这个主要是多线程程序会导致CPU频繁的切换线程,所以,不能片面的说多核的CPU就比单核的CPU速度快。对于目前最新的Core i5的四核CPU,比如2.0G频率,在硬件上已经做了优化,如果运行的主要是单线程的程序,那么它会把运行频率提高到3.0或者其它的频率,同时关闭其它的硬核,以提高运行速度。而在主要运行多线程的程序时,它会根据算法平均分配CPU资源以加快程序运行的效率。。。
2、对于多线程程序的编写,一定要尽量少而精简的利用线程,以减少CPU对线程的调度切换的时间和效率。
3、除了使用.NET框架提供的线程操作处理类方法外,还有其它第3方的解决方案,比如Intel就提供了第3方的组件来提供支持,这个可以参考《多核程序设计技术》一书。
4、ASP.NET程序的运行,本身就是多线程的,所以,如果可以,建议查阅该方面底层的内容,对.NET框架如何应用多线程技术,以及如何提高效率做参考。
5、可以查阅其它相关C#线程操作方面的书籍。或者找些C#写的网游游戏代码来做参考。这些都是多线程技术的典型应用方向。
时间过得真快,转眼又到周末了,祝大家周末愉快吧。该休息的休息,该玩的玩。。。。。。