浅谈.NET下的多线程和并行计算(十二)CLR via C#第三版阅读笔记(1)

最近此书出了第三版,在阅读此书线程部分的过程中有很多心得,补充了此前知识盲点,因此把这些关键和重要的知识点汇集成日志文章并且纳入到这个系列中。顺便说一下,笔者喜欢这本书的原因是作者作为微软顾问并没有按照MSDN的教条教大家怎么去用而是能说出很多自己的观点甚至很多是微软.NET框架不够的地方,并给出自己的实现。

 

为什么说线程是比较昂贵的?

1) 从内存上来说,(对于32位架构)每一个线程包含线程内核对象(700字节)/线程环境块(4KB)/内核堆栈(12KB)/用户堆栈(1MB)。并且可以发现,这1MB的用户堆栈内存在CLR线程创建的时候完全分配,并不是动态增加的(Windows线程的创建只是保留1MB的内存空间)。

2) 从线程切换上来说,需要做哪些步骤来进行切换?首先是把CPU寄存器中的值保存到当前线程的内核对象中,然后如果线程切换到不同CPU的话需要为CPU准备新的虚拟地址空间,最后把目标线程内核对象中寄存器的值复制到CPU寄存器中。

3) 更大的性能损害来自于,线程切换之后缓存中的数据可能会不能命中,需要重新准备这些数据。

4) 此外,在垃圾回收的时候,CLR会挂起所有线程,查看线程堆栈,垃圾回收压缩后重置堆栈指针地址。

当然,线程总比进程的创建好一点,不过作者也说了线程多啊,一个OUTLOOK有几十个线程,作者还纳闷怎么打开记事本的打开对话框会多22个线程,我想这个和操作系统有关,Vista的打开文件对话框更复杂,在其中为了不阻塞UI很多Part都以新的线程来加载内容,还好这个打开对话框用的不是CLR线程……当前CLR线程对应Windows线程,作者也希望在将来CLR能实现虚拟逻辑线程的概念,以改善性能。

 

什么时候手动创建线程而不是使用线程池

1) 需要自定义线程的优先级,线程池的线程总是Normal。

2) 需要一个前台线程,线程池的线程总是后台线程。

作者建议大家对于非UI线程创建为后台线程而不是前台线程,有的时候我们可以发现有些软件在关闭之后,或者说关闭UI之后在进程中还存在,占用内存,这是因为我们看到关闭的是UI线程,还有其它前台线程未关闭。

3) 需要手动中止线程,线程池不提供这个功能。

4) 线程执行时间很长,线程池用于短而多的线程任务比较合适。

 

线程的调度

1) 每一个线程的优先级是0到31。高优先级的线程ready之后,不管低优先级的线程在做什么,立即上位,没话说。Windows会把最高优先级的不同线程调度到各个CPU上并行执行,多核多处理器谁也不闲着。

2) Windows制定进程有6个优先等级,线程有7个,通过组合来得出实际的线程优先级0到30(0优先级保留给Windows用于内存释放)。CLR保留了线程优先级中的最低和最高级,供程序员可设置的只有5个等级。

3) 进程的优先级是一个虚拟的概念,只是为了帮助用于映射到1-31中的某个等级,一般来说进程的等级默认为创建它的进程的等级。很多进程都是Windows Explorer创建的,默认也就是Nomral这个等级,说白了我们的线程在大多情况下映射到Windows线程优先级为6-10。

 

CLR线程池

1) 最小线程数,线程池的线程总大于等于这个值,一般这个值设置为逻辑CPU数,也就是能充分利用CPU同时执行这些线程。

2) 最大线程数,默认1000,不建议修改这个值,如果这个值过小,很可能运行的线程的都被阻塞,而排队的线程永远得不到执行,作者甚至建议CLR团队放开这个限制,让线程池尽可能允许创建更多线程一直到内存溢出。

3) 线程池是非常智能的,并不会发现可用线程不够马上创建新的线程,而是会有一个延迟以确保真的需要新的线程来补充(因为也不建议线程池中的方法执行时间太长比如超过500毫秒,影响线程池的判断)。线程池的目的就是减少实际线程的创建和回收,重复利用线程来做不同的工作。

 

有关APM

1) 即使不需要得到异步操作结果也建议调用EndXXX一次并且只是一次。一是用于捕获操作产生的异常二是用来释放分配的内存。

2) 作者认为异步调用产生的小对象在频繁调用的时候对性能有影响,如果操作确实执行时间很多,同步调用也罢。

3) 并不是所有的IO操作都原生支持异步的,比如创建文件,列目录等,对于这些操作只能通过新线程来模拟异步(占用CPU)。

4) 大部分异步IO不支持取消,即使操作系统支持FCL也不一定把这个功能纳入,其实我们也明白很多时候FCL做的是对Windows API的封装,这个封装的覆盖可能不全也可能没那么及时。

 

有关EAP

 

1) 可以看出作者很不喜欢EAP,作者认为EAP主要作用是方便IDE中进行拖拽使用,并且支持汇报线程工作在UI线程。作者认为MSDN上的那篇文章的一些指导原则大部分人都不同意,因为那文章是Winform组人写的。

2) 作者认为EAP消耗更多的内存,并且使用起来也不方便。因为注册事件的代码如果在开始异步方法之后可能会导致异步方法不能执行,而且如果需要更换处理方法的话还需要反注册事件。并且EAP在处理异常上也不方便。在.NET 4.0中提供的Task能大大改善异步模式的易用性。

 

有关IO操作的异步

 

1) 我们知道计算密集的操作我们可以通过开启多个线程充分利用多核来提高系统性能。而对于IO操作如果仅仅采用多线程的话也仅仅能起到不卡住UI线程的作用,由于IO操作时间长,也就会需要大量的新线程来处理。硬件和操作系统对很多IO行为支持IOCP,这样能进行IO操作的时候不占用线程。那么我们也就特别需要注意怎么去使用API来开启IOCP的支持,而不能一味认为使用APM就是使用了IOCP(比如FileStream的使用注意FileOptions.Asynchronous开关)。

2) 即使支持了IOCP,我们也要知道如何正确的去使用APM,在BeginXXX后立即EndXXX显然不是合理的使用方式,这就相当于说“你去做这件事情吧,然后我还跟在你后面看着你做”。

posted @ 2010-02-24 17:21  lovecindywang  阅读(4406)  评论(4编辑  收藏  举报