CLR Via C# 3rd 阅读摘要 -- Chapter 26 – Compute-Bound Asynchronous Operations
Introducing the CLR's Thread Pool
- 线程池可以看成有一组线程可以被你的应用程序以自有的方式使用;
- 每个CLR有一个线程池,这个线程池被该CLR控制的AppDomain共享;
- 当CLR初始化时,线程池中没有线程。线程池内部维护一个操作请求的队列;
- 线程池最重要的作用是管理拥有多少线程才算合适,少点线程以避免浪费资源,多点线程以发挥多处理器、超线程处理器、多核处理器的优势;
- 工作者线程(Worker Threads)用来当应用程序请求线程池来执行异步的计算方向的操作(也可以初始化一个I/O方向的操作);
- I/O线程(I/O Threads)用来通知你的代码一个异步的I/O操作已经完成;
- APM:Asynchronous Programming Model。
Performing a Simple Compute-Bound Operation
- TheadPool.QueueUserWorkerItem(WaitCallback callback), .QueueUserWorkerItem(WaitCallback callback, Object state);
- 如果回调方法抛出一个异常没有被处理,CLR终止该进程(除非宿主强制使用自己的策略)。
Execution Contexts
- 每个线程都有一个相关联的执行上下文(Execution Context)数据结构,包括如安全设置(Thread.Principal)、宿主设置(System.Threading.HostExecutionContextManager)、逻辑调用上下文数据(Sytem.Runtime.Remoting.Messaging.CallContext.LogicalSetData(). LogicalSetData());
- ExecutionContext类允许你控制一个线程的Execution Context从一个线程产生自另一个线程;
- ExecutionContext.SuppressFlow()方法标记有[SecurityCritical]属性,因此一些客户程序(比如SilverLight)不能调用;
- 流转包含逻辑调用上下文数据项的执行上下文会戏剧性的影响性能,因为需要序列化/反序列化这些数据项;
- 在执行ExecutionContext.SuppressFlow()之后,CallContext.LogicalSetData()设置的上下文数据就不能被访问了。
Cooperative Cancellation
- 合作取消模式,意味着你希望取消的操作必须支持显示的取消行为;
- 要在线程池中取消一个操作,必须使用System.Threading.CancellationTokenSource对象。实例化CancellationTokenSource对象后,该实例有一个Token属性;
- 可以注册(CancellationToken.Register())一个或多个方法,当一个CancellationTokenSource取消后被调用;
- 如何连接多个CancellationTokenSource对象,调用CancellationTokenSource.CreateLinkedTokenSource()静态方法。
Tasks
- 使用ThreadPool.QueueUserWorkerItem()虽然简单,但是有限制,最大的问题是不知道何时操作完成,也没有办法获得返回值;
- 更好的解决方案是使用Tasks,System.Threading.Tasks;
- [Flags]public enum TaskCreationOptions {None, PreferFairness, LongRunning, AttachedToParent};
- 等待一个Task完成并获得返回结果:Task.Wait()实例方法, Task.Result属性;
- 如果调用了Wait(),但是Task没有开始执行,系统可能(依赖TaskScheduler)使用调用Wait()的线程执行该Task,这样线程调用Wait()不会被阻塞;立即执行Task然后立即返回,这可能会节约资源提高性能,但是可能会造成死锁;
- 使用TaskScheduler.UnobservedTaskException静态事件注册回调方法,可以帮组检测未观测到的异常;
- Task.WaitAny()静态方法阻塞调用线程直到数组中的任何Task对象完成;
- Task.WaitAll()静态方法阻塞调用线程直到数组中的所有Task对象完成;
- 取消一个Task:Token.ThrowIfCancellationRequested();
- 如果CancellationToken在Task开始之后被调度之前取消,Task不执行就取消。但是如果不开始就取消,会抛出InvalidOperationException异常;
- 一旦一个Task对象有CancellationToken关联上,就没有途径访问它,所以你必须以某种方式获得相同的CancellationToken;
- 当一个Task完成时自动开始另一个Task:Task.ContinueWith()实例方法;
- [Flags]public enum TaskContinuationOptions {None, PreferFairness, LongRunning, AttachedToParent, ExecuteSynchronously, NotOnRanToCompletion, NotOnFaulted, NotOnCanceled, OnlyOnCanceled, OnlyOnFaulted, OnlyOnRanToCompletion};
- 一个Task开始多个Child Tasks:Task
; - public enum TaskStatus {Created, WaitingForActivation, WaitingToRun, Running, WaitingForChildrenToComplete, RanToCompletion, Canceled, Faulted};
- TaskFactory
, .StartNew(); - 两类TaskScheduler:
- 线程池任务调度器:默认情况下,所有的应用程序使用线程池任务调度器;
- 同步上下文任务调度器:通常用于Windows Forms、WPF、SilverLight等GUI应用;
- Parallel Extensions Extras包:
- IOTaskScheduler
- LimitedConcurrencyLevelTaskScheduler
- OrderedTaskScheduler
- PrioritizingTaskScheduler
- ThreadPerTaskScheduler
Parallel's Static For, ForEach, and Invoke Methods
- 如果工作必须以顺序方式完成,那么就不要使用Parallel方法;同样,避免工作项修改任何类型的共享数据,因为如果多个线程同时维护该数据可能会出现问题;
- Parallel.For(), .ForEach(), .Invoke()方法可以接受ParallelObject对象参数;
- Parallel.For(), .ForEach()的重载方法可以传入3个委托:
- 任务本地初始化委托(localInit),在任务请求处理一个工作项前调用;
- 主体部分委托(body),一旦有项目被参与工作的线程处理时被调用;
- 任务本地终止委托(localFinally),任务处理完所有的工作项后调用,即使遇到异常。
Parallel Language Integrated Query
- 使用LINQ,可以简便的进行过滤、排序、投影等操作。但是这些处理过程是顺序的;
- Parallel LINQ,以并行的方式使用LINQ。System.LINQ.ParallelQuery
; - 要使用Parallel LINQ,必须使用ParallelEnumerable.AsParallel()扩展方法从顺序查询(基于IEnumerable或IEnumerable
)转换成并行查询(基于ParallelQuery或ParallelQuery ); - 如果需要让Parallel LINQ保持处理的顺序,需要调用ParallelEnumerable.AsOrdered()扩展方法;
- 以无序方式处理的行为:Distinct, Except, Intersect, Union, Join, GroupBy, GroupJion, ToLookup;
- 以有序方式处理的行为:OrderBy, OrderbyDescending, ThenBy, ThenByDescending;
- WithDegreeOfParallelism()方法指定同时处理的最多的线程数量。缺省情况下,线程数量等于CPU核的数量;
- Parallel LINQ会分析查询然后决定如何处理最佳;
- 有时候顺序处理会获得最佳性能,比如:Concat, ElementAt(OrDefault), First(OrDefault), Last(OrDefault), Skip(While), Take(While), Zip。以及在选择器(selector)和断言(predicate)委托中含有位置指示的Select(Many)和Where;
- 如果你希望强制并行方式处理,可以调用WithExecutionMode(),传入ParallelExecutionMode {Default, ForceParallelism}标志;
- WithMergeOptions(),可以用来控制查询结果集合并缓存,也就是时空权衡,传入ParallelMergeOptions {Default, NoBuffered, AutoBuffered, FullyBuffered}标志;
Performing a Periodic Compute-Bound Operation
- System.Threading.Timer,构造时可以有TimerCallback回调参数;
- 当一个Timer对象被当作垃圾收集后,它的终止化方法告诉线程池取消该定时器因此不再离开。因此当使用Timer对象时,确定有变量保持Timer对象存活否则你的回调方法将停止调用;
- 有很多的定时器,之间有什么差异呢?
- System.Threading.Timer,最好的定时器,如果要执行间隙性的后台任务,首选它;
- System.Windows.Forms.Timer,类似于Win32的SetTimer函数,当定时器消失时,Windows会插入一个WM_TIMER在线程的消息队列中。不能被多线程同时执行。回调方法的线程必须和设置定时器的线程是同一线程;
- System.Threading.DispatcherTimer,用于WPF和SilverLight,等同于System.Windows.Forms.Timer;
System.Timer.Timer,唯一的好处就是可以用于在设界面计器上拖拽,除非真的有必要,否则就不要用。
How the Thread Pool Manages Its Threads
- 线程池的默认最多线程数量是1000,这在32位系统2G的内存地址空间下是比较合理的限制。CLR项目组会进行合理调整,不要自行修改;
- System.Threading.ThreadPool.GetMaxThreads(), .SetMaxThreads(), .GetMinThreads(), .SetMinThreads(), .GetAvailableThreads();
- CLR的线程池有一个全局队列,每个工作线程有一个本地队列;
- 全局队列采用FIFO算法;本地队列采用LIFO算法。
Cache Lines and False Sharing
- CPU逻辑上将内存划分为缓存行(cache line),在64位的机器系统上,一个缓存行包含64字节,因此CPU以64字节块对RAM进行存取;
- 因此,如果是64位的机器系统,需要读取Int32的变量,那么包含该变量(4Bytes)的64Bytes的内存块会被读取。读取更多的字节可能会有很好的性能提升,因为大多数应用程序趋向于访问临近的数据,这样数据就很可能已经在Cache中,不需要再从RAM中读取;
- 但是,如果如果两核或多核访问的数据在同一个缓存行上,这些CPU核就需要进行协调,反而会极大的对性能造成负面影响;
- Win32的GetProcessorInformation()函数可以获得CPU的缓存行大小;
- [StructLayout(LayoutKind.Explicit)]和[FieldOffest(...)]以缓存行大小递增可以控制字段的布局,以避免需要多线程访问的临近数据不在同一个缓存行上;
- 因为内存布局和缓存行的缘故。从程序的观点,两个线程管理不同的数据。但是从CPU的观点,两个CPU核却是管理相同的数据。这叫做:虚假共享(False Sharing);
- 如果这些CPU在不同的NUMA节点上,虚假共享(False Sharing)会对性能造成极其负面的影响;
- 要避免虚假共享(False Sharing),应该避免一个线程写数组的前几个元素而其他的线程在访问数组的其他元素。
本章小结
本章讲了计算方向的异步操作问题,首先隆重推出了CLR的线程池概念,给线程池的线程划分了两个类别:工作者线程和I/O线程。然后演示了如何简单的使用线程池实现一个多线程计算操作;接着讨论了线程池的线程执行体上下文结构;然后引出了协作式取消模式以解释如何在线程池中取消操作;本章重点讨论了Task,一种使用线程池的更强大易用方法,并演示了如何等待任务完成获得结果,如何取消任务,如何在一个任务完成后自动的开始另外一个任务,如何由一个任务开始子任务。接着分析了Task的实现机制和调度策略,如何利用TaskFactory工厂模式创建任务。本章还讲述了如何使用Parallel LINQ以及它的限制和注意事项;接着还讨论了定时器的作用,比较了FCL提供的几种定时器的差异。之后简单解释了线程池是如何管理线程的。最后讨论了高速缓存行和多处理器环境下需要注意的错误共享(False Sharing)问题。