CLR via C# 笔记 -- 计算限制的异步操作(27)
1. 线程池基础。
创建和销毁线程是一个昂贵的操作,要耗费大量时间。太多的线程会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以大多的线程还对性能不利。为了改善这个情况,CLR包含了代码来管理它自己的线程池(thread pool)。线程池是你的应用程序能使用的线程集合。每CLR一个线程池;这个线程池有CLR控制的所有AppDomain共享。如果一个进程中加载了多个CLR,那个每个CLR都有它自己的线程池。
CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池线程。如果线程池中没有线程,就创建一个新线程。创建线程会造成一个的性能损失。然而,当线程池线程完成任务后,线程不会被销毁。相反,线程会返回线程池,在那里进入空闲状态,等待响应另一个请求。由于线程不销毁自身,所以不再产生额外的性能损失。
如果你的应用程序向线程池发出许多请求,线程池会尝试只用这一个线程来服务所有请求。然而,如果你的应用程序发出请求的速度超过了线程池处理它们的速度,就会创建额外的线程。最终,你的应用程序的所有请求都能由少量线程处理,所以线程池不必创建大量线程。
如果你的应用程序停止向线程池发出请求,池中会出现大量什么都不做的线程。这是对内存资源的浪费。所以,当一个线程池线程闲着没事儿一段时间之后(不同版本的CLR对这个时间的定义不同),线程会自己醒来终止自己以释放资源。线程终止自己会产生一定的性能损失。然而,线程终止自己是因为它闲得慌,表明应用程序本身就没有做太多得事情,所以这个损失关系不大。
线程池可以只容纳少量线程,从而避免浪费资源;也可以容纳更多的线程,以利用多处理器、超线程处理器和多核处理器。它能在这两种不同的状态之间从容地切换。线程池是启发式地。如果应用程序需要执行许多任务,同时有可用地CPU,那么线程池会创建更多地线程。应用程序负载减轻,线程池线程就终止它们自己。
2. 执行简单地计算限制操作
要将一个异步地计算限制操作放到线程池地队列中,通常可以调用ThreadPool类地QueueUserWorkItem方法。这些方法向线程池的队列添加一个”工作项“(work item)以及可选的状态数据。然后,所有方法会立即返回。工作项其实就是有callBack参数标识的一个方法,该方法将由线程池线程调用。可向方法传递一个state实参(状态数据)。无state参数的那个版本的QuereUserWorkItem则向回调方法传递null。最终,池中的某个线程会处理工作项,造成你指定的方法被调用。
3. 执行上下文。包含有安全设置(压缩栈、Thread的Principal属性和Windows身份)、宿主设置以及逻辑调用上下文数据。线程执行它的代码时,一些操作会受到线程执行上下文设置的影响。理想情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该流向(复制到)辅助线程。这就确保了辅助线程执行的任何操作使用的是相同的安全设置和宿主设置。还确保了在初始线程的逻辑调用上下文中存储的任何数据都适用于辅助线程。默认情况下,CLR自动造成初始线程的执行上下文”流向“任何辅助线程。这造就将上下文信息传给辅助线程,但这会对性能造成一定影响。这是因为执行上下文包含大量信息,而收集所有这些信息,再把它们复制到辅助线程,要耗费不少时间。如果辅助线程又采用了更多的辅助线程,还必须创建和初始化更多的执行上下文数据结构。可以通过ExecuteContext.SuppressFlow()阻止执行上下文流动
static void Main(string[] args) { // 将一些数据放到Main线程的逻辑调用上下文中 CallContext.LogicalSetData("Name", "Jeffrey"); // 初始化要由一个线程池线程做一些工作 // 线程池线程能访问逻辑调用上下文数据 ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // 现在,阻止Main线程的执行上下文流动 ExecutionContext.SuppressFlow(); // 初始化要由线程池做的工作,线程池线程不能访问逻辑调用上下文数据 ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // 恢复Main线程的执行上下文的流动, // 以免将来使用更多的线程池线程 ExecutionContext.RestoreFlow(); Console.ReadLine(); }
4. 协作式取消和超时
System.Threading.CancellationTokenSource 对象包含了和管理取消有关的所有状态。可以从它的Token属性获取得一个或多个CancellationToken实例,并传给你得操作,使操作可以取消。可定时调用CancellationToken的IsCancellationRequested属性,了解循环是否应该提前终止,从而终止计算限制的操作。
public static void Go() { CancellationTokenSource cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); Console.WriteLine("Press <Enter> to cancel the operation."); Console.ReadLine(); cts.Cancel(); Console.ReadLine(); } private static void Count(CancellationToken token, Int32 countTo) { for (int count = 0; count < countTo; count++) { if (token.IsCancellationRequested) { Console.WriteLine("Count is cancelled"); break; // 退出循环以停止操作 } Console.WriteLine(count); Thread.Sleep(200); // 出于演示目的而浪费一些时间 } Console.WriteLine("Count is done"); }
要执行一个不允许被取消的操作,可向该操作传递通用调用CancellationToken的静态None属性而返回的CancellationToken。该属性返回一个特殊的CancellationToken实例,它不和任何CancellationTokenSource对象关联(实例的私有字段为null)。由于没有CancellationTokenSource,所以没有代码能调用Cancel。一个操作如果查询这个特殊CancellationToken的isCancellationRequested属性,将总是返回 false。
如果愿意,可调用CancellationTokenSource的Register方法等级一个或多个在取消一个CancellationTokenSource时调用的方法。要向方法传递一个Action<object>委托;一个要通过委托传给回调(方法)的状态值;以及一个Boolean值(名为useSynchronizationContext),该值指明是否要使用调用线程的SynchornizationContext来调用委托。如果为useSynchronizationContext参数传递false,那么调用Cancel的线程会顺序调用已登记的所有方法。为useSynchronizationContext参数传递true,则回调(方法)会被调用send(而不是post)给已捕捉的SynchronizationContext对象,后者决定由哪个线程调用回调(方法)。多次调用Register,多个回调方法都会调用。这些回调方法可能抛出未处理的异常。如果调用CancellationTokenSource的Cancel方法,向它传递ture,那么抛出了未处理异常的第一个回调方法会阻止其他回调方法的执行,抛出的异常也会从Cancel中抛出。如果调用Cancel并向它传递false,那么登记的所有回调方法都会调用。所有未处理的异常都会添加到一个集合中。所有回调方法都执行好后,其中任何一个抛出了未处理的异常,Cancel就会抛出一个AggregateException,该异常实例的InnerExceptions属性被设为已抛出的所有异常对象的集合。如果登记的所有回调方法都没有抛出未处理的异常,那么Cancel直接返回,不抛出任何异常。
// 创建一个CancellationTokenSource var cts1 = new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine("cts1 canceled")); // 创建一个CancellationTokenSource var cts2 = new CancellationTokenSource(); cts2.Token.Register(() => Console.WriteLine("cts2 canceled")); // 创建一个新的CancellationTokenSource,它在cts1或cts2取消时取消 var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); linkedCts.Token.Register(() => Console.WriteLine("linkedCts canceled")); // 取消其中一个CancellationTokenSource(我选择cts2) cts2.Cancel(); Console.WriteLine("Cts1 canceled={0}, cts2 canceled={1}, linkedCts={2}", cts1.IsCancellationRequested, cts2.IsCancellationRequested, linkedCts.IsCancellationRequested);
// linkedCts canceled // cts2 canceled // Cts1 canceled=False, cts2 canceled=True, linkedCts=True
5. Task。由于ThreadPool的QueueUserWorkItem方法没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获得返回值。所以引入System.Threading.Tasks。
public enum TaskCreationOptions { None = 0x0000, // 默认 // 提议TaskScheduler你希望任务尽快执行 PreferFairness = 0x0001, // 提议TaskScheduler应尽可能地创建线程池线程 LongRunning = 0x0002, // 该提议总是被采纳:将一个Task和它的父Task关联 AttachedToParent = 0x0004, // 该提议总是被采纳:将一个Task和它的父任务连接,它就是一个普通任务,而不是子任务 DenyChildAttach = 0x0008, // 该提议总是被采纳:强迫子任务使用默认调度器而不是父任务的调度器 HideScheduler = 0x0011, }
static void Main(string[] args) { Task<int> t = new Task<int>(n => Sum((int)n), 100000000); t.Start(); t.Wait(); // 抛出 System.AggregateException 异常 Console.WriteLine("The sum is: " + t.Result); } private static int Sum(int n) { int sum = 0; for (; n > 0; n--) { checked { sum += n; } // 抛出 System.OverflowExceptions异常 } return sum; }
线程调用Wait方法时,系统检查线程要等待的Task是否已开始执行。如果是,调用Wait的线程会阻塞,直到Task运行结束为止。但如果Task还没开始执行,系统可能(取决于TaskScheduler)使用调用Wait的线程来执行Task。在这种情况下,调用Wait的线程不会阻塞;它会执行Task并立即返回。好处在于,没有线程会被阻塞,所以减少了对资源的占用(因为不需要创建一个线程来替换被阻塞的线程),并提升了性能(因为不需要花时间创建线程,也没有上下文切换)。不好的地方在于,假如线程在调用Wait前已获得了一个线程同步锁,而Task试图获取同一个锁,就会造成死锁的线程。
如果一直不调用Wait或Result,或者一直不查询Task的Exception属性,代码就一直注意不到这个异常的发生,这当然不好,因为程序一道了未预料到的问题,而你居然没注意到。为了帮助你检测没有被注意到的异常,可以向TaskScheduler的静态UnobservedTaskException事件登记一个回调方法。每次当一个Task被垃圾回收时,如果存在一个没有被注意到的异常,CLR的终结器线程就会引发这个事件。一旦引发,就会向你的事件处理方法传递一个UnobservedTaskExceptionEventArgs对象,其中包含你没注意到AggregateException。
Task.WaitAny方法会阻塞调用线程直到数组中的任何Task对象完成。方法返回Int32数据索引值,指明完成的是那个Task对象。方法返回后,线程被唤醒并继续运行。如果发生超时,方法返回-1。如果WaitAny通过一个CancellationToken取消,会抛出一个OperationCanceledException。
Task.WaitAll方法会阻塞调用线程,直到数组中的所有Task对象完成。如果所有Task对象都完成。如果所有Task对象都完成,WaitAll方法返回true。发生超时则返回false。如果WaitAll方法通过一个CancellationToken取消,会抛出一个OperationCanceledException。
6. 取消任务
private static int Sum(CancellationToken ct, int n) { int sum = 0; for (; n > 0; n--) { // 在取消标志引用的CancellationTokenSource上调用Cancel,下面这行代码就会抛出OperationCanceledException ct.ThrowIfCancellationRequested(); checked { sum += n; } // 抛出 System.OverflowExceptions异常 } return sum; }
循环(负责执行计算限制的操作)中调用CancellationToken的 ThrowIfCancellationRequested 方法定时检查操作是否已取消。这个方法与 CancellationToken 的 IsCancellationRequested属性相似。但如果CancellationTokenSource已经取消,ThrowIfCancellationRequested会抛出一个OperationCanceledException。之所以选择抛异常,是因为和ThreadPool的QueueUserWorkItem方法初始化的工作项不同,任务有办法表示完成,任务甚至能返回一个值。所以,需要采取一种方式将已完成的任务和出错的任务区分开。
static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task<int> t = Task.Run(() => Sum(cts.Token, 1000000), cts.Token); cts.Cancel(); try { Console.WriteLine("The sum is: " + t.Result); } catch (AggregateException ae) { ae.Handle(e => e is OperationCanceledException); Console.WriteLine("Sum was canceled"); } Console.ReadLine(); }
可在创建Task时将一个CancellationToken传给构造器,从而将两者关联。如果CancellationToken在Task调度前取消,Task会被取消,永远都不执行。但如果Task已调度(通过调用Start方法),那么Task的代码只有显式支持取消,其操作才能在执行期间取消。遗憾的是,虽然Task对象关联了一个CancellationToken,但却没有办法访问它。因此,必须在Task的代码中获取创建Task对象时的同一个CancellationToken。为此,最简单的方法就是使用一个lambda表达式,将CancellationToken作为闭包变量”传递“。
7. 完成任务时自动启动新任务
// 创建并启动一个Task,继续另一个任务 Task<int> t = Task.Run(() => Sum(CancellationToken.None, 100000)); // ContinueWith 返回一个Task,但一般都不需要在使用该对象 Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));
[Flags, Serializable] public enum TaskContinuationOptions { None = 0x0000, // 默认值 PreferFairness = 0x0001, // 提议TaskScheduler你希望该任务尽快执行 LongRunning = 0x0002, // 提议 TaskScheduler应尽可能地创建线程池线程 AttachedToParent = 0x0004, // 该提议总是被采纳:将一个Task和它父Task关联 DenyChildAttach = 0x0008, // 任务试图和这个父任务连接将抛出一个 InvalidOperationException HideScheduler = 0x0010, // 强迫子任务使用默认调度器而不是父任务的调度器 LazyCancellation = 0x0020, // 除非前置任务(antecedent task)完成,否则禁止延迟任务完成(取消) ExecuteSynchronously = 0x8000, // 这个标志指出你希望由执行第一个任务的线程执行 ContinueWith 任务。第一个任务完成后,调用 ContinueWith 的线程接着执行 ContinueWith 任务 // 这些标志指出在什么情况下运行ContinueWith任务 NotOnRanToCompletion = 0x1000, NotOnFaulted = 0x2000, NotOnCanceled = 0x40000, // 这些标志是以上三个标志的便利组合 OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted, OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled, OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled }
调用ContinueWith时,可用TaskContinuationOptions.OnlyOnCanceled标志指定新任务只有在第一个任务被取消时才执行。TaskContinuationOptions.OnlyOnFaulted标志指定新任务只有在第一个任务抛出未处理的异常时才执行。TaskContinuationOptions.OnlyOnRanToCompletion标志指定新任务只有在第一个任务顺利完成(中途没有取消,也没有抛出未处理异常)时才执行。默认情况下,如果不指定上述任何标志,则新任务无论如何都会运行,不管第一个任务如何完成。一个Task完成时,它的所有未运行的延续任务都被自动取消。
Task<int> t = Task.Run(() => Sum(10000)); t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result), TaskContinuationOptions.OnlyOnRanToCompletion); t.ContinueWith(task => Console.WriteLine("Sum threw:" + task.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted); t.ContinueWith(task => Console.WriteLine("Sum was canceled"), TaskContinuationOptions.OnlyOnCanceled);
8.任务内部揭秘
每个Task对象都有一组字段,这些字段构成了任务的状态。其中包括一个int32 ID(参见Task的只读Id属性)、代表Task执行状态的一个Int32、对父任务的引用、对Task创建时指定的TaskScheduler的引用、对回调方法的引用、对要传给回调方法的对象的引用(可通过Task的只读AsyncState属性查询)、对 ExecutionContext的引用以及对ManualResultEventSlim对象的引用。另外,每个Task对象都有对根据需要创建的补充猪骨浓汤的引用。补充状态包含一个CancellationToken、一个ContinueWithTask对象集合、为抛出未处理异常的子任务而准备的一个Task对象集合等。说了这么多,重点在于虽然任务很有用,但它并不是没有代价的。必须为所有这些状态分配内存。如果不需要任务的附加功能,那么使用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。
Task 和Task<TResult>类实现了IDisposable接口,允许在用完Task对象后调用Dispose。如今,所有Dispose方法所做的都是关闭ManualResultEventSlim对象。但可定义从Task和Task<TResult>派生的类,在这些类中分配它们自己的资源,并在它们重写的Dispose方法中释放这些资源。我建议不要在代码中从Task对象显示调用Display;相反,应该让垃圾回收器自己清理任何不再需要的资源。
/// <summary> /// 这些标志指出一个Task在其生命周期内的状态 /// </summary> public enum TaskStatus { Created, // 任务已显示创建;可以手动Start()这个任务 WaitingForActivation, // 任务已隐式创建;会自动开始 WaitingToRun, // 任务已调度,但尚未运行 Running, // 任务正在运行 WaitingForChildrenToComplete, // 任务正在等待它的子任务完成,子任务完成后它才完成 RanToCompletion, Canceled, Faulted }
首次构造Task对象时,它的状态是Created。以后,当任务启动时,它的状态变成WaitingToRun。Task实际在一个线程上运行时,它的状态变成Running。任务停止运行,并等待它的任何子任务时,状态变成WaitingForChildrenToComplete。任务完成时进入以下状态之一:RanToCompletion(运行完成),Canceled(取消)或Faulted(出错)。如果运行完成,看通过Task<TResult>的Result属性来查询任务结果。Task或Task<TResult>出错时,可查询Task的Exception属性来获取任务抛出的未处理异常;该属性总是返回一个AggregateException对象,对象的InnerExceptions集合包含了所有未处理的异常。
Task提供了几个只读Boolean属性,包括IsCanceled,IsFaulted和IsComleted,IsCompleted返回true。
ContinueWith,ContinueWhenAll,ContinueWhenAny或FromAsync等方法来创建的Task对象处于WaitingForActivation状态。通过构造TaskCompletionSource<TResult>对象来创建Task也处于WaitingForActivation状态。不可显式启动通过调用ComtinueWith来创建对象。该Task在它的前置任务(antecedent task)执行完毕后自动启动。
9. 任务工厂。有时需要创建一组共享相同配置的Task对象。为避免机械的将相同的参数传给每个Task的构造器,可创建一个任务工厂来封装通用的配置。System.Threading.Tasks命名空间定义了一个TaskFactory类型和一个TaskFactory<TResult>类型。
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, completedTasks => completedTasks.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中,并只调用Console.WriteLine一次, // 因为这个任务可能和上面的任务并行执行,而我不希望任务的输出变得不连续 StringBuilder sb = new StringBuilder("The following exception(s) occurred: " + Environment.NewLine); foreach (var e in p.Exception.Flatten().InnerExceptions) { sb.AppendLine(" " + e.GetType().ToString()); Console.WriteLine(sb.ToString()); } }, TaskContinuationOptions.OnlyOnFaulted); parent.Start(); Console.ReadLine();
调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法时,以下TaskContinuationOption标志时非法的:NotOnRanToCompletion,NotOnFaulted 和 NotOnCanceled。当然,基于这些标志组合起来的标志(OnlyOnCanceled,OnlyOnFaulted和OnlyonRanToCompletion)也时非法的。也就是说,无论前置任务是如何完成的,ContinueWhenAll和ContinueWhenAny都会执行延续任务。
10.TaskScheduler对象负责执行被调度的任务,提供了两个派生类型:线程池任务调度(thread pool task scheduler)和同步上下文任务调度器(synchronization context task scheduler)。默认情况下使用线程池任务调度。可查询TaskScheduler的静态Default属性来获得对默认任务调度器的引用。
11. Parallel的静态For,ForEach和Invoke方法
// 线程池的线程并行处理工作 Parallel.For(0, 1000, i => DoWork(i)); // 线程池的线程并行处理工作 Parallel.ForEach(collection, item => DoWork(item)); // 线程池的线程并行执行方法 Parallel.Invoke(() => Method1(), () => Method2(), () => Method3());
如果任何操作抛出未处理的异常,你调用的Parallel方法最后会抛出一个AggregateException。
参与工作的每个任务都获得它自己的parallelLoopState对象,并可通过这个对象和参与工作的其他任务进行交互。Stop方法告诉循环停止处理任何更多的工作,未来对IsStopped属性的查询会返回true。Break方法告诉循环不再继续处理当前项之后的项。例如,假如ForEach被告知要处理100项,并在处理第5项时调用了Break,那么循环会确保前5项处理好之后,ForEach才返回。但要注意,这并不是说在这100项中,只有前5项才会被处理,第5项之后的项可能在以前以及处理过了。LowestBreakIteration属性返回在处理过程中调用过Break方法的最低的项。如果从来没有调用过Break,LowestBreakIteration属性会返回null。处理任何一项时,如果造成未处理的异常,IsException属性会返回true。处理一项时花费太长时间,代码可查询ShouldExitCurrentIteration属性看它是否应该提前退出。如果调用过Stop,调用过Break,取消过CancellationTokenSource(由ParallelOption的CancellationToken属性引用),或者处理一项时造成了未处理的异常,这个属性就会返回true。
12. 并行语言集成查询 Linq.AsParallel()
13. System.Threading.Timer
在内部,线程池为所有Timer对象只使用了一个线程。这个线程知道下一个Timer对象在什么时候到期(计时器还有多久触发)。下一个Timer对象到期时,线程就会唤醒,在内部调用ThreadPool的QueueUserWorkItem,将一个工作项添加到线程池的队列中,使你的回调方法得到调用。如果回调方法的执行时间很长,计时器可能(在上个回调还没完成的时候)再次触发。这可能造成多个线程池线程同时执行你的回调方法。为了解决这个问题,我的建议是:构造Timer时,为period参数指定Timeout.Infinite。这样,计时器就只触发一次,然后,在你的回调方法中,调用Change方法来指定一个新的DutTime,并再次为period参数指定Timeout.Infinite。
TImer对象被垃圾回收时,它的终结代码告诉线程池取消计时器,使它不再触发。所以,使用Timer对象时,要确定有一个变量在保持Timer对象的存活,否则对你的回调方法的调用就会停止。
private static Timer s_timer; static void Main(string[] args) { Console.WriteLine("Checking status every 2 seconds"); // 创建但不启动计时器。确保s_timer在线程池线程调用Status之前引用该计时器 s_timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite); // 现在s_timer已被赋值,可以启动计时器了, // 现在在Status中调用Change,保证不会抛出NullReferenceException s_timer.Change(0, Timeout.Infinite); Console.ReadLine(); } private static void Status(object state) { // 这个方法由一个线程池线程执行 Console.WriteLine("In Status at {0}", DateTime.Now); Thread.Sleep(1000); // 返回前让Timer在2秒后再次触发 s_timer.Change(2000, Timeout.Infinite); // 这个方法返回后,线程回归池中,等待下一个工作项 }
14. 计时器总结
System.Threading.Timer: 要在一个线程池线程上执行定时的(周期性发生后)后台任务,它是最好的计时器。
System.Windows.Forms.Timer: 构造这个类的实例,相当于告诉Windows将一个计时器的调用线程关联(参见Win32 SetTimer函数)。当这个计时器触发时,Windows将一条计时器消息(WM_TIMER)注入线程的消息队列。线程必须执行一个消息泵来提取这些消息,并把它们派发给需要的回调方法。注意,所有这些工作都只由偶一个线程完成———设置计时器的线程保证就时执行回调方法的线程。还意味着计时器方法不会由多个线程并发执行。
System.Windows.Threading.DispatcherTimer类: Silverlight和WPF应用程序中的等价物。
System.Timers.Timer:本质上时System.Threading.Timer类的包装类,计时器到器(触发)会导致CLR将事件放到线程池队列中。System.Timers.Timer类派生自System.ComponentModel.Component类。建议你也不要用它,除非真的想在设计平面上添加一个计时器。
15. 线程池如何管理线程
CLR允许开发人员设置线程要创建的最大线程数。但实践证明,线程池永远都不应该设置线程数上限,因为可能会发生饥饿或死锁。32位进程大约1360个线程。
System.Threading.ThreadPool类提供几个方法设置;GetMaxThreads、SetMaxThreads、GetMinThreads、SetMinThreads和GetAvailableThreads。
ThreadPool.QueueUserWorkItem方法和Timer类总是将工作项放到全局队列中。工作者线程采用一个先进先出(first-in-first-out, FIFO)算法将工作项从这个队列中取出,并处理它们。由于多个工作者线程可能同时从全局队列中拿走工作项,所以所有工作者线程都竞争一个线程同步锁,以保证两个或多个线程不会获取同一个工作项。这个线程同步锁在某些应用程序中可能成为瓶颈,对伸缩性和性能造成某种程度的限制。
非工作者线程调度一个Task时,该Task被添加到全局队列。但每个工作者线程都有自己的本地队列。工作者线程调度一个Task时,该Task被添加到调用线程的本地队列。
工作者线程准备好处理工作项时,它总是检查本地队列来查找一个Task。存在一个Task,工作者线程就从本地队列移除Task并处理工作项。要注意的是,工作者线程采用后入先出(LIFO)算法将任务从本地队列取出。由于工作者线程是唯一允许访问它自己的本地队列头的线程,所以无需同步锁,而且在队列中添加和删除Task的速度非常快。
线程池从来不保证排队中的工作项的处理顺序。这是合理的,尤其是考虑到多个线程可能同时处理工作项。但上述副作用使这个问题变得恶化了。你必须保证自己得应用程序对于工作项或Task的执行顺序不作任何预设。
如果工作者线程发现它的本地队列变空了,会尝试从另一个工作者线程的本地队列”偷“一个Task。这个Task是从本地列尾部”偷“走的,并要求获取一个线程同步锁,这对性能有少许影响。当然,希望这种”偷盗“行为很少发生,从而很少需要获取锁。如果所有本地队列都变空,那么工作者线程会使用FIFO算法,从全局队列提取一个工作项(取得它得锁)。如果全局队列也为空,工作者线程会进入睡眠状态,等待事情的发生。如果睡眠了长时间,它会自己醒来,并销毁自身,允许系统回收线程使用的资源(内核对象、栈、TEB等)。
线程池会快速创建工作者线程,使工作者线程的数据等于传给ThreadPool的SetMinThreads方法值,如果从不调用这个方法,那么默认值等于你的进程允许使用的CPU数量,这是由进程的affinity mask 关联掩码决定的。通常,你的进程允许使用机器上的所有CPU,所以线程池创建的工作者线程数量很快就会达到机器的CPU数。创建了这么多(CPU数量)的线程后,线程池会监视工作项的完成速度。如果工作项完成的时间太长,线程池会创建更多的工作者线程。如果工作项的完成速度开始变块,工作者线程会被销毁。