读书笔记—CLR via C#线程25-26章节
前言
这本书这几年零零散散读过两三遍了,作为经典书籍,应该重复读反复读,既然我现在开始写博了,我也准备把以前觉得经典的好书重读细读一遍,并且将笔记整理到博客中,好记性不如烂笔头,同时也在写的过程中也可以加深自己理解的深度,当然同时也和技术社区的朋友们共享
线程
- 线程内部组成
- 线程内核对象 thread kernel object,在该结构中,包含一组对线程进行描述的属性。数据结构中还包括所谓的线程上下文thread context。上下文是一个内存块,包含了CPU的寄存器集合,占用几百到几千个字节的内存
- 线程环境块 thread environment block,TEB 是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化一个内存块。TEB耗用1个内存页。(86和64中时4KB,IA64CPU中是8KB)。TEB包含线程的异常处理链首。线程进入每个try块都在链首插入一个节点。线程退出try块时,会从链中删除该节点。TEB还包含线程的“线程本地存储”数据,以及由GDI和OpenGL图像使用的一些数据结构
- 用户模式栈,存储传给方法的局部变量和实参。它还包含一个地址,指出当前方法返回时,线程接着应该从什么地方开始执行。默认情况下,Windows为每个线程的用户模式栈分配1MB内存
- 内核模式栈,应用程序代码向操作系统中的一个内核模式的函数传递实参时,还会使用内核模式栈。32位Windows占12KB,64位则为24KB
- DLL线程连接和分离通知,attach和detach通知,托管DLL不会收到通知,这提升了性能
- 针对线程的设计: 可靠性+响应能力 换取 速度+性能
- 专用线程的使用场景
- 非普通优先级
- 前台线程
- 长时间运行
- 需要显式终止
上下文切换
- 上下文切换步骤:
- 将CPU寄存器中的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中
- 从现有线程集合中选出一个线程调度,如果该线程由另一个进程拥有,Windows在执行代码或接触数据之前,还必须切换CPU“看见”的虚拟地址空间
- 将所选上下文结构中的值加载到CPU的寄存器中
- Windows大约每30毫秒执行一次上下文切换。上下文的净开销,通过牺牲性能换来好的用户体验
- 执行上下文切换所需的时间取决于CPU的架构和速度
- 如果要构建高性能的应用程序,应该尽量避免上下文切换
计算限制的异步操作
- 优点: 1)在GUI中保持UI的可响应性 2)多个CPU缩短一个耗时计算所需的事件,提高伸缩性和吞吐量
线程池
- 每个CLR一个线程池,这个线程池由CLR控制的所有AppDomain共享
- 如果一个进程加载多个CLR,那么每个CLR都有自己的线程池
- 线程池不销毁自身,所以不再产生额外的性能损失。当一个线程池闲着没事儿做有一段时间之后,线程会自己醒来终止自己以释放资源。此时比较空闲,所以性能损失影响不大
- 线程池内部拥有Worker线程或IO线程
- APM异步编程模型发出IO请求,比如文件,网络,数据库,web服务或其他硬件设备
执行上下文
- 线程关联的一个数据结构,包含安全设置(压缩栈,Principal和Windows身份)、宿主设置以及逻辑调用上下文(CallContext)数据
- 初始线程执行上下文应该流向(复制到)辅助线程。这确保辅助线程执行的操作时相同的安全设置和宿主设置。还确保初始线程的逻辑调用上下文可以在辅助线程中使用。(这对性能有一定的影响)
- 可以使用ExecutionContext的方法阻止上下文流动,提升应用程序的性能。这在APM,TASK中也适用
协作式取消的支持
- 对于长时间使用的线程,应该支持“取消”
- CancellationTokenSource. 调用方适用cts.Cancel方法,线程内判断IsCancellationRequested属性
- 如果不允许取消,则传递CancellationToken.None属性返回的对象,CanBeCanceled属性会返回false
- 取消对象支持Register回调方法。同步调用(同步上下文为true,send)情况下顺序调用,第一个回调异常会终止,否则为post情况下回调方法都会调用,未处理的异常都会添加道一个集合中。通过AggregateException,InnerExceptions属性为异常集合。cts.Token.Register(()=>...) cts.Cancel().... 支持取消连接合并。协作式取消
Task
- 相比较,ThreadPool的异步操作,没有回调通知的机制。通过任务比较简单
- new Task(action, para).Start(); t.Wait()显式等待 t.Result获取结果(结果属性内部会调用Wait)
- 如果抛出异常,会被“侵吞”并存储到一个集合中,调用Wait或Result时,抛出AggregateException(Handle表示异常是否处理)
- WaitAny和WaitAll等待多个任务
- 检测任务没被注意到的异常,向TaskScheduler的静态UnobservedTaskException事件登记回调。Task被GC回收时如果有没被注意的异常,CLR的终结器会引发这个事件。可调用SetObserved方法指出以及处理好了异常,从而阻止CLR终止进程(根据实际情况考虑是否登记事件并处理)
- Task的链式操作,ContinueWith可以比较好的进行非阻塞的回调
- 任务支持父/子关系
- Task的内部构造
- Int32的TaskID
- Task执行状态
- 对父任务的引用
- 对Task创建时指定TaskSchedule的引用
- 对回调方法的引用
- 对要传递回调方法的对象的引用(AsyncState)
- 对一个ExecutionContext的引用
- 对一个ManualResetEventSlim的引用
- 其他补充状态的引用(CancellationToken,ContinuWithTask等等)
- 所以Task通过牺牲一定的性能来提供更加方便和丰富的异步操作
- Task的Dispose主要为关闭ManualResetEventSlim对象
- 在IDE中通过“并行任务”或“并行堆栈”窗口中找到自己的任务
- 任务的生命周期
- 首次构造Task,状态为Created
- 启动任务之后,状态变为WaitingToRun
- 实际在线程上运行后,状态变为Running
- 任务停止等待子任务,状态为WaitingForChildrenToComplete
- 任务结束时,状态可能为:RunToCompletion, Canceled或者Faulted
- 任务完成时,通过Result属性获取结果,任务出错时,通过Exception获取异常
- 提供简化属性,IsCanceled, IsFaulted, IsCompleted(取消或者出错时这里是true)
- 判断任务执行成功,应该用 task.Status == TaskStatus.RunToCompletion
- 如果Task通过调用ContinuWith,ContinueWhenAll,ContinueWhenAny或者FromAsync,状态为WaitingForActivation,自动开始调度
- 通过任务工厂TaskFactory来共享状态,代码如下:
static void Main(string[] args) { Task parent = new Task(() => { var cts = new CancellationTokenSource(); var tf = new TaskFactory<int>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); // 创建并启动3个子任务 var childTasks = new[]{ tf.StartNew(()=>Sum(cts.Token,10000)), tf.StartNew(()=>Sum(cts.Token,20000)), tf.StartNew(()=>Sum(cts.Token,int.MaxValue)) }; // 任务子任务抛出异常,就取消其余子任务 for (int task = 0; task < childTasks.Length; task++) { childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); } // 所有子任务完成后,从未出错/未取消的任务获取返回的最大值 // 然后将最大值传给另一个任务来显式最大结果 tf.ContinueWhenAll(childTasks, completionTasks => completionTasks.Where( t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None) .ContinueWith(t => Console.WriteLine("The maximum is: " + t.Result), TaskContinuationOptions.ExecuteSynchronously); }); // 子任务完成时,也显式任务未处理的异常 parent.ContinueWith(p => { StringBuilder sb = new StringBuilder( "The following exception(s) occured: " + Environment.NewLine); foreach (var e in p.Exception.Flatten().InnerExceptions) { sb.AppendLine(" " + e.GetType().ToString()); } Console.WriteLine(sb.ToString()); }, TaskContinuationOptions.OnlyOnFaulted); // 启动父任务,便于它启动它的子任务 parent.Start(); Console.Read(); } static int Sum(CancellationToken token, int number) { int sum = 0; for (; number > 0; number--) { token.ThrowIfCancellationRequested(); checked { sum += number; } } return sum; }
任务调度器
负责执行调度的任务,为任务提供基础框架
- FCL提供两个派生自TaskSchedule的类型,线程池任务调度器和同步上下文任务调度器
- 默认情况使用线程池任务调度器(TaskSchedule.Default)
- 同步上下文任务调度器通常用于Presentation展示层,将所有任务都调度给应用程序的GUI线程,使所有任务代码都能成功更新UI组件,比如按钮和控件,同步上下文任务调度器根本不适用线程池,可以通过TaskSchedule.FromCurrentSychronizationContext方法获取对一个同步上下文任务调度器的引用
- 有空可以下载其他的任务调度器进行研究和使用,http://code.msdn.microsoft.com/ParExtSamples
- IOTaskSchedule 将任务队列到线程池的IO线程,而不是它的工作者线程
- LimitedConcurrencyLevelTaskSchedule 此任务调度器不允许超过一定数目的任务同时执行
- OrderedTaskScheduler 一次只允许执行一个任务,派生自上面的调度器,为n传递1
- PriortizingTaskScheduler 将任务调度到CLR的线程池,之后可以控制线程相对于普通的优先级
- ThreadPerTaskScheduler 此任务调度器为每个任务创建并启动一个单独的线程,完全不使用线程池
简化的并发编程
System.Threading.Tasks.Parallel封装了Task的使用,并行执行工作项
- Parallel.For(0,1000,i=>DoWork(I ));
- Parallel.Foreach(collection, item=>DoWork( item ));
- Parallel.Invoke(()=>Method1(), ()=>Method2());
- For方法比Foreach方法快
- 可以允许指定配置,ParallelOptions,包含取消操作、最大工作性、任务调度器
- 可以允许传递局部初始化委托、主体委托、任务局部终结委托
- For和Foreach方法都返回一个ParallelLoopResult实例,包括循环是否完成等情况
PLINQ
- 将集合中的数据项的处理工作分散到多个CPU上,以便并发处理多个数据项
- 静态System.Linq.ParallelEnumerable类实现了PLINQ的所有功能,公开了所有标识LINQ操作符的并行版本,所有这些方法都是System.Linq.ParallelQuery<T>类型的扩展方法。将顺序查询转变为并行查询,只需要调用ParallelEnumerable的AsParallel扩展方法,如果进行反向操作,则使用AsSequential方法
- 如果需要并行遍历查询的结果,使用ParallelEnumerable的ForAll方法
- 如果需要让PLINQ并发调用保持数据项的顺序,可调用ParallelEnumerable的AsOrdered方法,反之就是AsUnOrdered方法
- 其他:WithCancellation,WithDegreeOfParallem
- 并行LINQ处理完数据项合并回去调用 WithMergeOptions,传递标志位指定缓冲和合并方式
定时器
System.Threading.Timer
- 在内部,线程池为所有Timer对象只使用了一个线程。这个线程知道下一个TImer对象在什么时候到期,Timer对象到期时,线程就会唤醒在内部调用ThreadPool.QueueUserWorkItem,将一个工作项添加到线程池的队列中
- 如果回调方法执行时间过长,计时器可能再次触发,造成线程池分配多个线程同时执行回调。为解决这个问题,可以在构造Timer时为period参数指定Timeout.Infinite。这样计时器只触发一次,然后在回调方法中调用Change方法指定一个新的dueTime,并再次为period指定Timeout.Infinite
- Timer类提供Dispose方法,允许完全取消计时器,并可在当时处于pending状态的所有回调完成之后,向notifyObject参数标识的内核对象发出信号
- 一个Timer对象被垃圾回收时,终结器代码告诉线程池取消计时器,使他不再触发。所以使用Timer时,最好定义一个变量保持Timer对象的存活,否则对回调方法的调用就会停止
- 经过测试,定时器被Disposed终结的时候,会执行定时器指定的回调方法一次
- 其他计时器
- System.WIndows.Forms.Timer,将一个计时器与调用线程关联,计时器触发时,Windows将计时器消息注入线程的消息队列,线程执行一个消息泵来提取消息。并把他们派遣给需要调用的回调方法。所有工作都只由一个线程完成,设置计时器的线程保证就是执行回调方法的线程。所以计时器方法也不会由多个线程并发执行
- System.WIndows.Threading.DispatcherTimer类,和Forms的定时器一样,等价
- System.Timers.Timer,对System.Threading.Timer的包装,计时器到期时,导致CLR将事件放到线程池的队列中。一般用于界面上拖放控件使用,不建议使用
线程池的使用
- 默认最大值1000个线程
- 32位系统最大2GB的可用地址空间,加载Win32和CLR DLLs,分配本地堆和托管堆之后,剩余1.5G,每个线程占用超过1MB,最多差不多1360个线程,超出则OutofMemoryException
- 64位进程提供8TB的地址空间,理论上可以创建千百万个线程。
- JR的建议是不要调用线程池的静态方法
- MinThreads的默认值为进程允许使用的CPUS数,通常你的进程允许使用机器上的所有CPU,所以线程池创建的工作者线程数量很快就会达到机器的CPU数、
Tips
- 执行垃圾回收时,CLR必须挂起(暂停)所以线程,遍历他们的栈来查找根一边对堆中的对象进行标记(第一阶段),再次遍历他们的栈,再恢复所有线程。所以减少线程数量也会提升垃圾回收器的性能
- Jefferey貌似小小的吐槽了一下WIndows的记事本的设计,呵呵
- 线程很“贵”,需要合理理性使用它们
- 将CLR线程和window线程区别对待,以后的趋势,编码简单,提升性能
- P/Invoke本地线程, System.Threading.Thread.BeginThreadAffinity和 Thread.EndTheradAffinity方法来通知CLR
- 你不能保证自己的线程一直运行,你不能阻止其他线程的运行。因为线程上下文的切换
- 0优先级的线程为零页线程,没有其他进程时,将系统RAM的所有空闲页清零
- CLR的终结器线程以Time-Critical优先级运行
- 一个进程中的所有前台线程停止运行时,CLR强制终止运行的任何后天线程。直接终止,不会抛出异常
- 所有前台线程终止,所有应用程序退出,整个进程就可以被销毁了
- 尽量避免使用前台线程
- 缓存线和伪共享,我表示,JR对Windows内核的熟悉程度,我望尘莫及,记录一下表示膜拜
该文仅仅作为笔记,需要详细了解线程相关的东西,可以继续参考:
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。