异步/多线程/任务/并行编程之一:如何选择合适的多线程模型?
异步、多线程、任务、并行编程之一:选择合适的多线程模型
本篇概述:
@fcl4.0中已经存在的线程模型,以及它们之间异同点;
@多线程编程模型的选择。
1:异步、多线程、任务、并行的本质
这四个概念对应在clr中的本质,本质都是多线程。
异步,简单的讲就是begininvoke、endinvoke模式,它在clr内部线程池进行管理;
多线程,体现在c#中,可以由类型thread发起。也可以由threadpool发起。前者不受clr线程池管理,后者则是。fcl团队为了各种编程模型的方便,还另外提供了backgroundworker和若干个timer,基本上它们都是threadpool的加强,增加了一些和调用者线程的交互功能;
任务(task),为fcl4.0新增的功能,在一个称之为任务并行库(tpl)的地方,其实也就是system.threading.tasks命名空间下。任务并行库名字取的很玄乎,其实它也是clr线程池的加强。优化了线程间的调度算法,增加了和调用者线程的交互功能;
并行(parallel),为fcl4.0新增的功能,也属于tpl。并行在后台使用task进行管理,说白了,因为task使用的线程池线程,所以parallel自然使用的也是线程池线程进行管理,它的本质仅仅是进一步简化了task。在这里要增进一个对于并行的理解。实际上,多线程天然就是并行的。及时不用任务并行库,用thread类型新起两个线程,clr或者说windows系统也会将这两个线程根据需要安排到两个cpu上去执行。所以,并不是因为多了任务并行库,clr才支持并行计算,任务并行库只是提供了一组api,使我们能够更好的操纵线程进行并行开发而已。
2:遗憾
jeffrey richter大叔说,微软提供了这么多线程模型,是遗憾的,因为这制造了混乱。很多开发者都不知道该选用哪个类型来编写自己的多线程代码。我们对微软总是又爱又恨,它总是不停的更新一些东西,逼迫我们不停的学习。但是也好,进步导致它不会过早死掉,让我们彻底失掉饭碗。
c#刚出来的被人笑,现在它的很多语法特性已经比java优美。很多时候我们太擅长于嘲笑,以致最后只能哭。顺便说一句,我依然是那么的喜欢java,只是很久没用它而已。
3:现在,该用什么来编写多线程
如果你在framework4.0下编写代码,那么应该按照这个优先级来撰写多线程代码:
优先 |
次优先 |
不得以 |
parallel(含扩展库plinq) task |
threadpool(backgroundworker,timer) 异步 |
thread |
这个表满足了大部分情况下的一个优先级指导,但在某些情况下会有例外。
3.1:为什么 parallel和task优先级一样,而不是优于task?
parallel虽然在后台使用task进行管理,并且它所谓简化了对于task的操作,但是它有一个重要的特征区别与task:parallel会阻滞调用者线程。查看paralle的成员,有for、foreach、invoke方法,它甚至都没提供一个begininvoke方法,也很好的暗示了这一点。不过虽然是同步的执行的,parallel还是会把多个任务分配到多个cpu上去。
task被用的最多的是start方法,它不会阻滞主线程。虽然task也提供了同步的启动线程的方法runsynchronously,但一般用的不多。
3.2:何时用异步,何时用线程或线程池
这需要从“io操作的dma(direct memory access)模式”讲起。通过dma的数据交换几乎可以不损耗cpu的资源。在硬件部分,硬盘、网卡、声卡、显卡等都有dma功能。可以简单的认为,当我们的工作线程需要操作i/o资源的时候(如读取一个大文件、读取一个网页、读取socke包等),我们就需要用异步去做这些事情。异步模式只会在工作开始以及工作结束的时候占用clr线程池,其它时候由硬盘、网卡等硬件设备来处理具体的工作,这就不会过多占用到cpu空间和时间损耗。
概括而言:
计算密集型工作,直接采用线程;
io密集型工作,采用异步机制;
当我们不清楚什么工作是i/o密集型的,一个不是很恰当的指导就是:查看fcl类型成员,如果成员提供了类似begindosomething方法的,则优先使用它,而不是新起一个线程或丢到线程池。
3.3:线程池的优势
新起线程,会带来很大的开销,这些开销主要集中在:分配线程内核对象、线程环境块、用户模式栈、内核模式栈所需要的内存空间,加载的dll的dllmain方法,并传递连接标志,以及线程上下文切换。由于线程如此昂贵,所以对于普通的开发要求来说,线程池就是一个很好的选择。线程池替开发人员管理工作线程,当一项工作完毕的时候,clr不会销毁这个线程,而是会保留这个线程一段时间,看是否有别的工作需要这个线程。至于何时销毁或新起线程,由clr决定。
3.4:何时用thread
以上的各种线程模型,它们最终都是thread。 那么什么时候需要thread直接出场呢?
最重要的使用thread的理由是,我们需要控制线程的优先级。thread之上的线程模型都不支持优先级设置。设置一个线程的高优先级可以使它获得更多的cpu时间;
再者,可以控制线程为前台线程。当然,由thread新起的线程默认就是前台线程。前台线程不随着调用者线程的中断而中断,这使得我们可以用thread来进行一些关键性的操作。