CLR via C#, 4th -- 【线程处理】 -- 第27章计算限制的异步操作
27.1 CLR线程池基础
线程池(thread pool)
由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。为了改善这个情况,CLR包含了代码来管理它自己的线程池(thread pool)。线程池是你的应用程序能使用的线程集合。
每CLR一个线程池;这个线程池由CLR控制的所有AppDomain共享。如果一个进程中加载了多个CLR,那么每个CLR都有它自己的线程池。
如果线程池中没有线程,就创建一个新线程。创建线程会造成一定的性能损失。然而,当线程池线程完成任务后,线程不会被销毁。相反,线程会返回线程池,在那里进入空闲状态,等待响应另一个请求。由于线程不销毁自身,所以不再产生额外的性能损失。
27.2 执行简单的计算限制操作
要将一个异步的计算限制操作放到线程池的队列中,通常可以调用ThreadPool类定义
static Boolean QueueUserWorkItem(WaitCallback callBack); static Boolean QueueUserWorkItem(WaitCallback callBack, Object state);
using System; using System.Threading; public static class Program { public static void Main() { Console.WriteLine("Main thread: queuing an asynchronous operation"); ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); Console.WriteLine("Main thread: Doing other work here..."); Thread.Sleep(10000); // Simulating other work (10 seconds) Console.WriteLine("Hit <Enter> to end this program..."); Console.ReadLine(); } // This method's signature must match the WaitCallback delegate private static void ComputeBoundOp(Object state) { // This method is executed by a thread pool thread Console.WriteLine("In ComputeBoundOp: state={0}", state); Thread.Sleep(1000); // Simulates other work (1 second) // When this method returns, the thread goes back // to the pool and waits for another task } }
27.3执行上下文
每个线程都关联了一个执行上下文数据结构,执行上下文(execution context)包括有:
- 安全设置(压缩栈、Thread的Principal属性和Windows身份)
- 宿主设置(参见System.Threading.HostExecutionContextManager)
- 逻辑调用上下文数据(参见System.Runtime.Remoting.Messaging.CallContext的LogicalSetData和LogicalGetData方法)
线程执行它的代码时,一些操作会受到线程执行上下文设置(尤其是安全设置)的影响。理想情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该流向(复制到)辅助线程。这就确保了辅助线程执行的任何操作使用的是相同的安全设置和宿主设置。还确保了在初始线程的逻辑调用上下文中存储的任何数据都适用于辅助线程。
默认情况下,CLR自动造成初始线程的执行上下文“流向”任何辅助线程。
System.Threading命名空间有一个ExecutionContext类,它允许你控制线程的执行上下文如何从一个线程“流”向另一个。
public sealed class ExecutionContext : IDisposable, ISerializable { [SecurityCritical] public static AsyncFlowControl SuppressFlow(); public static void RestoreFlow(); public static Boolean IsFlowSuppressed(); // Less commonly used methods are not shown }
可用这个类阻止执行上下文流动以提升应用程序的性能。对于服务器应用程序,性能的提升可能非常显著。但客户端应用程序的性能提升不了多少。
如果初始线程的执行上下文不流向辅助线程,辅助线程会使用上一次和它关联的任意执行上下文。
public static void Main() { // Put some data into the Main thread's logical call context CallContext.LogicalSetData("Name", "Jeffrey"); // Initiate some work to be done by a thread pool thread // The thread pool thread can access the logical call context data ThreadPool.QueueUserWorkItem( state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // Now, suppress the flowing of the Main thread's execution context ExecutionContext.SuppressFlow(); // Initiate some work to be done by a thread pool thread // The thread pool thread CANNOT access the logical call context data ThreadPool.QueueUserWorkItem( state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // Restore the flowing of the Main thread's execution context in case // it employs more thread pool threads in the future ExecutionContext.RestoreFlow(); ... Console.ReadLine(); }
27.4协作式取消和超时
Microsoft.NET Framework提供了标准的取消操作模式。这个模式是协作式的,意味着要取消的操作必须显式支持取消。
取消操作首先要创建一个System.Threading.CancellationTokenSource对象
public sealed class CancellationTokenSource : IDisposable { // A reference type public CancellationTokenSource(); public Boolean IsCancellationRequested { get; } public CancellationToken Token { get; } public void Cancel(); // Internally, calls Cancel passing false public void Cancel(Boolean throwOnFirstException); ... }
构造好一个CancellationTokenSource(一个引用类型)之后,可从它的Token属性获得一个或多个CancellationToken(一个值类型)实例,并传给你的操作,使操作可以取消。
public struct CancellationToken { // A value type public static CancellationToken None { get; } // Very convenient public Boolean IsCancellationRequested { get; } // Called by nonTask invoked operations public void ThrowIfCancellationRequested(); // Called by Task invoked operations // WaitHandle is signaled when the CancellationTokenSource is canceled public WaitHandle WaitHandle { get; } // GetHashCode, Equals, operator== and operator!= members are not shown public Boolean CanBeCanceled { get; } // Rarely used public CancellationTokenRegistration Register(Action<Object> callback, Object state, Boolean useSynchronizationContext); // Simpler overloads not shown }
CancellationToken实例是轻量级值类型,包含单个私有字段,即对其CancellationTokenSource对象的引用。
internal static class CancellationDemo { public static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); // Pass the CancellationToken and the numbertocountto into the operation ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); Console.WriteLine("Press <Enter> to cancel the operation."); Console.ReadLine(); cts.Cancel(); // If Count returned already, Cancel has no effect on it // Cancel returns immediately, and the method continues running here... Console.ReadLine(); } private static void Count(CancellationToken token, Int32 countTo) { for (Int32 count = 0; count <countTo; count++) { if (token.IsCancellationRequested) { Console.WriteLine("Count is cancelled"); break; // Exit the loop to stop the operation } Console.WriteLine(count); Thread.Sleep(200); // For demo, waste some time } Console.WriteLine("Count is done"); } }
注意
要执行一个不允许被取消的操作,可向该操作传递通过调用CancellationToken的静态None属性而返回的CancellationToken,该属性返回一个特殊的CancellationToken实例,它不和任何CancellationTokenSource对象关联(实例的私有字段为null),由于没有CancellationTokenSource,所以没有代码能调用Cancel。
CancellationTokenSource.Register
如果愿意,可调用CancellationTokenSource的Register方法登记一个或多个在取消一个CancellationTokenSource时调用的方法。要向方法传递一个Action<Objecp委托;一个要通过委托传给回调(方法)的状态值;以及一个Boolean值(名为useSynchronizationContext),该值指明是否要使用调用线程的SynchronizationContext来调用委托。如果为useSynchronizationContext参数传递false,那么调用Cancel的线程会顺序调用已登记的所有方法。为useSynchronizationContext参数传递true,则回调(方法)会被send(而不是post")给已捕捉的SynchronizationContext对象,后者决定由哪个线程调用回调(方法)。
如果执行send操作,要等到在目标线程那里处理完毕之后才会返回。在此期间,调用线程会被阻塞。这相当于同步调用。而如果执行post操作,是指将东西post到一个队列中便完事儿,调用线程立即返回,相当于异步调用。
多次调用Register,多个回调方法都会调用。这些回调方法可能抛出未处理的异常。如果调用CancellationTokenSource的Cancel方法,向它传递true,那么抛出了未处理异常的第一个回调方法会阻止其他回调方法的执行,抛出的异常也会从Cancel中抛出。如果调用Cancel并向它传递false,那么登记的所有回调方法都会调用。所有未处理的异常都会添加到一个集合中。所有回调方法都执行好后,其中任何一个抛出了未处理的异常,Cancel就会抛出一个AggregateException,该异常实例的InnerExceptions属性被设为已抛出的所有异常对象的集合。如果登记的所有回调方法都没有抛出未处理的异常,那么Cancel直接返回,不抛出任何异常。
CancellationToken的Register方法返回一个CancellationTokenRegistration,如下所示:
public struct CancellationTokenRegistration : IEquatable<CancellationTokenRegistration>, IDisposable { public void Dispose(); // GetHashCode, Equals, operator== and operator!= members are not shown }
可以调用Dispose从关联的CancellationTokenSource中删除已登记的回调;这样一来,在调用Cancel时,便不会再调用这个回调。
var cts = new CancellationTokenSource(); cts.Token.Register(() => Console.WriteLine("Canceled 1")); cts.Token.Register(() => Console.WriteLine("Canceled 2")); // To test, let's just cancel it now and have the 2 callbacks execute cts.Cancel();
最后,可以通过链接另一组CancellationTokenSource来新建一个CancellationTokensource对象。任何一个链接的CancellationTokenSource被取消,这个新的Cancellation TokenSource对象就会被取消。
// Create a CancellationTokenSource var cts1 = new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine("cts1 canceled")); // Create another CancellationTokenSource var cts2 = new CancellationTokenSource(); cts2.Token.Register(() => Console.WriteLine("cts2 canceled")); // Create a new CancellationTokenSource that is canceled when cts1 or ct2 is canceled var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); linkedCts.Token.Register(() => Console.WriteLine("linkedCts canceled")); // Cancel one of the CancellationTokenSource objects (I chose cts2) cts2.Cancel(); // Display which CancellationTokenSource objects are canceled Console.WriteLine("cts1 canceled={0}, cts2 canceled={1}, linkedCts canceled={2}", cts1.IsCancellationRequested, cts2.IsCancellationRequested, linkedCts.IsCancellationRequested);
有的时候,与其等待漫长时间获得一个完整的结果,还不如在短时间内报错,或者用部分计算好的结果进行响应。可调用CancellationTokenSource的CancelAfter方法。
public sealed class CancellationTokenSource : IDisposable { // A reference type public CancellationTokenSource(Int32 millisecondsDelay); public CancellationTokenSource(TimeSpan delay); public void CancelAfter(Int32 millisecondsDelay); public void CancelAfter(TimeSpan delay); ... }
27.5 任务
任务
很容易调用ThreadPool的QueueUserWorkltem方法发起一次异步的计算限制操作。但这个技术有许多限制。最大的问题是没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获得返回值。为了克服这些限制(并解决其他一些问题),Microsoft 引入了任务的概念。
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); // Calling QueueUserWorkItem new Task(ComputeBoundOp, 5).Start(); // Equivalent of preceding using Task Task.Run(() => ComputeBoundOp(5)); // Another equivalent
还可选择向构造器传递一些TaskCreationOptions标志来控制Task的执行方式。TaskCreationOptions枚举类型定义了一组可按位OR的标志。定义如下:
[Flags, Serializable] public enum TaskCreationOptions { None = 0x0000,// The default // Hints to the TaskScheduler that you want this task to run sooner than later. PreferFairness = 0x0001, // Hints to the TaskScheduler that it should more aggressively create thread pool threads. LongRunning = 0x0002, // Always honored: Associates a Task with its parent Task (discussed shortly) AttachedToParent = 0x0004, // If a task attempts to attach to this parent task, it is a normal task, not a child task. DenyChildAttach = 0x0008, // Forces child tasks to use the default scheduler as opposed to the parent’s scheduler. HideScheduler = 0x0010 }
27.5.1 等待任务完成并获取结果
private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n > 0; n ) checked { sum += n; } // if n is large, this will throw System.OverflowException return sum; }
// Create a Task (it does not start running now) Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000000000); // You can start the task sometime later t.Start(); // Optionally, you can explicitly wait for the task to complete t.Wait(); // FYI: Overloads exist accepting timeout/CancellationToken // You can get the result (the Result property internally calls Wait) Console.WriteLine("The Sum is: " + t.Result); // An Int32 value
Wait
线程调用Wait方法时,系统检查线程要等待的Task是否已开始执行。如果是,调用Wait的线程会阻塞,直到Task运行结束为止。但如果Task还没有开始执行,系统可能取决于TaskScheduler)使用调用Wait的线程来执行Task,在这种情况下,调用Wait的线程不会阻塞;它会执行Task并立即返回。
System.AggregateException
如果计算限制的任务抛出未处理的异常,异常会被“吞噬”并存储到一个集合中,而线程池线程可以返回到线程池中。调用Wait方法或者Result属性时,这些成员会抛出一个System.AggregateException对象。
AggregateException提供Flatten方法,它创建一个新的AggregateException,其InnerExceptions属性包含一个异常列表,其中的异常是通过遍历原始AggregateException的内层异常层次结构而生成的。
AggregateException还提供Handle方法,它为AggregateException中包含的每个异常都调用一个回调方法。然后,回调方法可以为每个异常决定如何对其进行处理;回调返回true表示异常已处理;返回false表示未处理。调用Handle后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常,并抛出这个新的AggregateException对象。
注意
如果一直不调用Wait或Result,或者一直不查询Task的Exception属性,代码就一直注意不到这个异常的发生。可以向TaskScheduler 的静态UnobservedTaskException事件登记一个回调方法。每次当一个Task被垃圾回收时,如果存在一个没有被注意到的异常,CLR的终结器线程就会引发这个事件。
WaitAny & WaitAll
除了等待单个任务,Task类还提供了两个静态方法,允许线程等待一个Task对象数组。
Task的静态WaitAny方法会阻塞调用线程,直到数组中的任何Task对象完成。方法返回Int32数组索引值,指明完成的是哪个Task对象。方法返回后,线程被唤醒并继续运行。如果发生超时,方法将返回-1。如果WaitAny通过一个CancellationToken取消,会抛出一个OperationCanceledException
Task类还有一个静态WaitAll方法,它阻塞调用线程,直到数组中的所有Task对象完成。如果所有Task对象都完成,WaitAll方法返回true。发生超时则返回false。如果WaitAll 通过一个CancellationToken取消,会抛出一个OperationCanceledException.
27.5.2 取消任务
可用一个CancellationTokenSource取消Task。
private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n > 0; n ) { // The following line throws OperationCanceledException when Cancel // is called on the CancellationTokenSource referred to by the token ct.ThrowIfCancellationRequested(); checked { sum += n; } // if n is large, this will throw System.OverflowException } return sum; }
之所以选择抛出异常,是因为和ThreadPool的QueueUserWorkItem方法初始化的工作项不同,任务有办法表示完成,任务甚至能返回一个值。所以,需要采取一种方式将已完成的任务和出错的任务区分开。而让任务抛出异常,就可以知道任务没有一直运行到结束。
CancellationTokenSource cts = new CancellationTokenSource(); Task<Int32> t = Task.Run(() => Sum(cts.Token, 1000000000), cts.Token); // Sometime later, cancel the CancellationTokenSource to cancel the Task cts.Cancel(); // This is an asynchronous request, the Task may have completed already try { // If the task got canceled, Result will throw an AggregateException Console.WriteLine("The sum is: " + t.Result); // An Int32 value } catch (AggregateException x) { // Consider any OperationCanceledException objects as handled. // Any other exceptions cause a new AggregateException containing // only the unhandled exceptions to be thrown x.Handle(e => e is OperationCanceledException); // If all the exceptions were handled, the following executes Console.WriteLine("Sum was canceled"); }
可在创建Task时将一个CancellationToken传给构造器(如上例所示),从而将两者关联。如果CancellationToken在Task调度前取消,Task会被取消,永远都不执行。但如果Task己调度(通过调用Start方法),那么Task的代码只有显式支持取消,其操作才能在执行期间取消。遗憾的是,虽然Task对象关联了一个CancellationToken,但却没有办法访问它。因此,必须在Task的代码中获得创建Task对象时的同一个CancellationToken。为此,最简单的办法就是使用一个lambda表达式,将CancellationToken作为闭包变量“传递”(就像上例那样)。
27.5.3任务完成时自动启动新任务
伸缩性好的软件不应该使线程阻塞。调用Wait,或者在任务尚未完成时查询任务的Result属性",极有可能造成线程池创建新线程,这增大了资源的消耗,也不利于性能和伸缩性。幸好,有ContinueWith方法可以知道一个任务在什么时候结束运行。任务完成时可启动另一个任务。
// Create and start a Task, continue with another task //调用静态Run方法会自动创建Task对象并立即调用Start Task<Int32> t = Task.Run(() => Sum(CancellationToken.None, 10000)); // ContinueWith returns a Task but you usually don't care //Result属性内部会调用Wait Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));
现在,执行Sum的任务完成时会启动另一个任务(也在某个线程池线程上)以显示结果。执行上述代码的线程不会进入阻塞状态并等待这两个任务中的任何一个完成。
ContinueWith返回对新Task对象的引用(我的代码是将该引用放到cwt变量中)。当然,可以用这个Task对象调用各种成员(比如Wait,Result,甚至Continuewith),但一般都忽略这个Task对象,不再用变量保存对它的引用。
Task对象内部包含了ContinueWith任务的一个集合。所以,实际可以用一个Task对象来多次调用ContinueWith。任务完成时,所有ContinueWith任务都会进入线程池的队列中。
此外,可在调用ContinueWith时传递对一组TaskContinuationOptions枚举值进行按位OR运算的结果。前6个标志(None,PreferFairness,LongRunning,AttachedToParent,DenyChildAttach和HideScheduler)与之前描述的TaskCreationOptions枚举类型提供的标志完全一致。
[Flags, Serializable] public enum TaskContinuationOptions { None = 0x0000,// The default // Hints to the TaskScheduler that you want this task to run sooner than later. PreferFairness = 0x0001, // Hints to the TaskScheduler that it should more aggressively create thread pool threads. LongRunning = 0x0002, // Always honored: Associates a Task with its parent Task (discussed shortly) AttachedToParent = 0x0004, // If a task attempts to attach to this parent task, an InvalidOperationException is thrown. DenyChildAttach = 0x0008, // Forces child tasks to use the default scheduler as opposed to the parent’s scheduler. HideScheduler = 0x0010, // Prevents completion of the continuation until the antecedent has completed. LazyCancellation = 0x0020, // This flag indicates that you want the thread that executed the first task to also // execute the ContinueWith task. If the first task has already completed, then the // thread calling ContinueWith will execute the ContinueWith task. ExecuteSynchronously = 0x80000, // These flags indicate under what circumstances to run the ContinueWith task NotOnRanToCompletion = 0x10000, NotOnFaulted = 0x20000, NotOnCanceled = 0x40000, // These flags are convenient combinations of the above three flags OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted, OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled, OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled, }
调用ContinueWith时,可用TaskContinuationOptions.OnlyOnCanceled标志指定新任务只有在第一个任务被取消时才执行。类似地,TaskContinuationOptions.OnlyOnFaulted标志指定新任务只有在第一个任务抛出未处理的异常时才执行。当然,还可使用TaskContinuationOptions.OnlyOnRanToCompletion标志指定新任务只有在第一个任务顺利完成(中途没有取消,也没有抛出未处理异常)时才执行。
// Create and start a Task, continue with multiple other tasks Task<Int32> t = Task.Run(() => Sum(10000)); // Each ContinueWith returns a Task but you usually don't care 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);
27.5.4任务可以启动子任务
Task<Int32[]> parent = new Task<Int32[]>(() => { var results = new Int32[3]; // Create an array for the results // This tasks creates and starts 3 child tasks new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start(); // Returns a reference to the array (even though the elements may not be initialized yet) return results; }); // When the parent and its children have run to completion, display the results var cwt = parent.ContinueWith( parentTask => Array.ForEach(parentTask.Result, Console.WriteLine)); // Start the parent Task so it can start its children parent.Start();
一个任务创建的一个或多个Task对象默认是顶级任务,它们与创建它们的任务无关。但TaskCreationOptions.AttachedToParent标志将一个Task和创建它的Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束。调用ContinueWith方法创建Task时,可指定TaskContinuationOptions.AttachedToParent标志将延续任务指定成子任务。
27.5.5 任务内部揭秘
每个Task对象都有一组字段,这些字段构成了任务的状态。
- 一个Int32 ID(参见Task的只读Id属性)
- 代表Task执行状态的一个Int32
- 对父任务的引用
- 对Task创建时指定的TaskScheduler的引用
- 对回调方法的引用
- 对要传给回调方法的对象的引用(可通过Task的只读AsyncState属性查询)
- 对ExecutionContext的引用
- 对ManualResetEventSlim对象的引用
每个Task对象都有对根据需要创建的补充状态的引用。补充状态包含:
- 一个CancellationToken
- 一个ContinueWithTask对象集合
- 为抛出未处理异常的子任务而准备的一个Task对象集合等
虽然任务很有用,但它并不是没有代价的。必须为所有这些状态分配内存。如果不需要任务的附加功能,那么使用ThreadPool.QueueUserWorkltem能获得更好的资源利用率。
Task和Task<TResult类实现了IDisposable接口,允许在用完Task对象后调用Dispose.如今,所有Dispose方法所做的都是关闭ManualResetEventSlim对象。建议不要在代码中为Task对象显式调用Dispose;相反,应该让垃圾回收器自己清理任何不再需要的资源。在一个Task对象的存在期间,可查询Task的只读Status属性了解它在其生存期的什么位置。该属性返回一个TaskStatus值:
public enum TaskStatus { // These flags indicate the state of a Task during its lifetime: Created, // Task created explicitly; you can manually Start() this task WaitingForActivation,// Task created implicitly; it starts automatically WaitingToRun, // The task was scheduled but isn’t running yet Running, // The task is actually running // The task is waiting for children to complete before it considers itself complete WaitingForChildrenToComplete, // A task's final state is one of these: RanToCompletion, Canceled, Faulted }
为简化编码,Task提供了几个只读Boolean属性,包括IsCanceled,IsFaulted和IsCompleted.注意当Task处于RanToCompletion,Canceled或Faulted状态时,IsCompleted返回true.判断一个Task是否成功完成最简单的办法是使用如下所示的代码:
if (task.Status == TaskStatus.RanToCompletion) ...
27.5.6任务工厂
有时需要创建一组共享相同配置的Task对象。为避免机械地将相同的参数传给每个Task的构造器,可创建一个任务工厂来封装通用的配置。
System.Threading.Tasks命名空间定义了一个TaskFactory类型和一个TaskFactory<TResul>类型。两个类型都派生自System.Object;也就是说,它们是平级的。
要创建一组返回void的任务,就构造一个TaskFactory;要创建一组具有特定返回类型的任务,就构造一个TaskFactory<TResult,并通过泛型TResult实参传递任务的返回类型。
创建上述任何工厂类时,要向构造器传递工厂创建的所有任务都具有的默认值。具体地说,要向任务工厂传递希望任务具有的CancellationToken,TaskScheduler TaskCreationOptions和TaskContinuationOptions设置。
Task parent = new Task(() => { var cts = new CancellationTokenSource(); var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); // This task creates and starts 3 child tasks var childTasks = new[] { tf.StartNew(() => Sum(cts.Token, 10000)), tf.StartNew(() => Sum(cts.Token, 20000)), tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) // Too big, throws OverflowException }; // If any of the child tasks throw, cancel the rest of them for (Int32 task = 0; task < childTasks.Length; task++) childTasks[task].ContinueWith( t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); // When all children are done, get the maximum value returned from the // nonfaulting/canceled tasks. Then pass the maximum value to another // task that displays the maximum result tf.ContinueWhenAll( childTasks, completedTasks => completedTasks.Where(t => t.Status == TaskStatus.RanToCompletion).Max(t => t.Result), CancellationToken.None) .ContinueWith(t =>Console.WriteLine("The maximum is: " + t.Result), TaskContinuationOptions.ExecuteSynchronously); }); // When the children are done, show any unhandled exceptions too parent.ContinueWith(p => { // I put all this text in a StringBuilder and call Console.WriteLine just once // because this task could execute concurrently with the task above & I don't // want the tasks' output interspersed 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); // Start the parent Task so it can start its children parent.Start();
注意 调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法时,以下TaskContinuationOption标志是非法的:NotOnRanToCompletion,NotOnFaulted和NotOnCanceled.当然,基于这些标志组合起来的标志(OnlyOnCanceled,OnlyOnFaulted和OnlyOnRanToCompletion)也是非法的。也就是说,无论前置任务是如何完成的,ContinuewhenAll和ContinueWhenAny都会执行延续任务。
27.5.7 任务调度器
任务基础结构非常灵活,其中TaskScheduler对象功不可没。TaskScheduler对象负责执行被调度的任务,同时向Visual Studio调试器公开任务信息。
FCL提供了两个派生自TaskScheduler的类型
线程池任务调度器(thread pool task scheduler)
默认情况下,所有应用程序使用的都是线程池任务调度器。这个任务调度器将任务调度给线程池的工作者线程。可查询TaskScheduler的静态Default属性来获得对默认任务调度器的引用。
同步上下文任务调度器(synchronization context task scheduler)
同步上下文任务调度器适合提供了图形用户界面的应用程序,它将所有任务都调度给应用程序的GUI线程,使所有任务代码都能成功更新UI组件,该调度器不使用线程池。可执行TaskScheduler的静态FromCurrentSynchronizationContext方法来获得对同步上下文任务调度器的引用。
internal sealed class MyForm : Form { private readonly TaskScheduler m_syncContextTaskScheduler; public MyForm() { // Get a reference to a synchronization context task scheduler m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Text = "Synchronization Context Task Scheduler Demo"; Visible = true; Width = 600; Height = 100; } private CancellationTokenSource m_cts; protected override void OnMouseClick(MouseEventArgs e) { if (m_cts != null) { // An operation is in flight, cancel it m_cts.Cancel(); m_cts = null; } else { // An operation is not in flight, start it Text = "Operation running"; m_cts = new CancellationTokenSource(); // This task uses the default task scheduler and executes on a thread pool thread Task<Int32> t = Task.Run(() => Sum(m_cts.Token, 20000), m_cts.Token); // These tasks use the sync context task scheduler and execute on the GUI thread t.ContinueWith(task => Text = "Result: " + task.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation canceled", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, m_syncContextTaskScheduler); t.ContinueWith(task => Text = "Operation faulted", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, m_syncContextTaskScheduler); } base.OnMouseClick(e); } }
单击窗体的客户区域,就会在一个线程池线程上启动一个计算限制的任务。使用线程池线程很好,因为GUI线程在此期间不会被阻塞,能响应其他UI操作。但线程池线程执行的代码不应尝试更新U1组件,否则会抛出InvalidOperationException。
计算限制的任务完成后执行三个延续任务之一。它们由与GU线程对应的同步上下文任务调度器来调度。任务调度器将任务放到GUI线程的队列中,使它们的代码能成功更新UI组件。所有任务都通过继承的Text属性来更新窗体的标题。
由于计算限制的工作(Sum)在线程池线程上运行,所以用户可以和UI交互来取消操作。在这个简单的例子中,我允许用户在操作进行期间单击窗体的客户区域来取消操作。
IOTaskScheduler
这个任务调度器将任务排队给线程池的1/0线程而不是工作者线程。
LimitedConcurrencyLevelTaskScheduler
这个任务调度器不允许超过n(一个构造器参数)个任务同时执行
OrderedTaskScheduler
这个任务调度器一次只允许一个任务执行。这个类派生自LimitedConcurrencyLevel TaskScheduler,为n传递1.
PrioritizingTaskScheduler
这个任务调度器将任务送入CLR线程池队列。之后,可调用Prioritize指出一个Task应该在所有普通任务之前处理(如果它还没有处理的话)。可以调用Deprioritize使一个Task在所有普通任务之后处理。
ThreadPerTaskScheduler
这个任务调度器为每个任务创建并启动一个单独的线程;它完全不使用线程池。
27.6 Parallel的静态For,ForEach和Invoke方法
System.Threading.Tasks.Parallel静态类
// One thread performs all this work sequentially for (Int32 i = 0; i < 1000; i++) DoWork(i);
使用Parallel类的For方法,用多个线程池线程辅助完成工作:
// The thread pool’s threads process the work in parallel Parallel.For(0, 1000, i => DoWork(i));
如果既可以使用For,也可以使用ForEach,那么建议使用For,因为它执行得更快。
// One thread performs all this work sequentially foreach (var item in collection) DoWork(item); // The thread pool's threads process the work in parallel Parallel.ForEach(collection, item => DoWork(item));
Parallel的所有方法都让调用线程参与处理。从资源利用的角度说,这是一件好事,因为我们不希望调用线程停下来(阻塞),等线程池线程做完所有工作才能继续。然而,如果调用线程在线程池线程完成自己的那一部分工作之前完成工作,调用线程会将自己挂起,直到所有工作完成。这也是一件好事,因为这提供了和使用普通for或foreach循环时相同的语义:线程要在所有工作完成后才继续运行。还要注意,如果任何操作抛出未处理的异常,你调用的Parallel方法最后会抛出一个AggregateException.
调用Parallel的方法时有一个很重要的前提条件:工作项必须能并行执行!所以,如果工作必须顺序执行,就不要使用Parallel的方法。另外,要避免会修改任何共享数据的工作项,否则多个线程同时处理可能会损坏数据。
注意Parallel的For,ForEach和Invoke方法都提供了接受一个ParallelOptions对象的重载版本。
public class ParallelOptions{ public ParallelOptions(); // Allows cancellation of the operation public CancellationToken CancellationToken { get; set; } // Default=CancellationToken.None // Allows you to specify the maximum number of work items // that can be operated on concurrently public Int32 MaxDegreeOfParallelism { get; set; } // Default= 1 (# of available CPUs) // Allows you to specify which TaskScheduler to use public TaskScheduler TaskScheduler { get; set; } // Default=TaskScheduler.Default }
除此之外,For和ForEach方法有一些重载版本允许传递3个委托。
- 任务局部初始化委托(localInit),为参与工作的每个任务都调用一次该委托。这个委托是在任务被要求处理一个工作项之前调用的。
- 主体委托(body),为参与工作的各个线程所处理的每一项都调用一次该委托。
- 任务局部终结委托(localFinally),为参与工作的每一个任务都调用一次该委托。这个委托是在任务处理好派发给它的所有工作项之后调用的。即使主体委托代码引发一个未处理的异常,也会调用它。
private static Int64 DirectoryBytes(String path, String searchPattern, SearchOption searchOption) { var files = Directory.EnumerateFiles(path, searchPattern, searchOption); Int64 masterTotal = 0; ParallelLoopResult result = Parallel.ForEach<String, Int64>( files, () => { // localInit: Invoked once per task at start // Initialize that this task has seen 0 bytes return 0; // Set taskLocalTotal initial value to 0 }, (file, loopState, index, taskLocalTotal) => { // body: Invoked once per work item // Get this file's size and add it to this task's running total Int64 fileLength = 0; FileStream fs = null; try { fs = File.OpenRead(file); fileLength = fs.Length; } catch (IOException) { /* Ignore any files we can't access */ } finally { if (fs != null) fs.Dispose(); } return taskLocalTotal + fileLength; }, taskLocalTotal => { // localFinally: Invoked once per task at end // Atomically add this task's total to the "master" total Interlocked.Add(ref masterTotal, taskLocalTotal); }); return masterTotal; }
每个任务都通过taskLocalTotal变量为分配给它的文件维护它自己的总计值。每个任务在完成工作之后,都通过调用Interlocked.Add方法(参见第29章“基元线程同步构造”),以一种线程安全的方式更新总的总计值(master total)。
只有在每个任务返回之后,masterTotal才需要以一种线程安全的方式更新masterTotal变量。所以,因为调用Interlocked.Add而造成的性能损失每个任务只发生一次,而不会每个工作项都发生。
注意,我们向主体委托传递了一个ParallelLoopState对象,
public class ParallelLoopState{ public void Stop(); public Boolean IsStopped { get; } public void Break(); public Int64? LowestBreakIteration{ get; } public Boolean IsExceptional { get; } public Boolean ShouldExitCurrentIteration { get; } }
Parallel的For和ForEach方法都返回一个ParallelLoopResult实例
public struct ParallelLoopResult { // Returns false if the operation was ended prematurely public Boolean IsCompleted { get; } public Int64? LowestBreakIteration{ get; } }
27.7 并行语言集成查询(PLINQ)
顺序查询(sequential query)
使用LINQ to Objects时,只有一个线程顺序处理数据集合中的所有项
并行LING(Parallel LINQ)
它将顺序查询转换成并行查询,在内部使用任务(排队给默认TaskScheduler),将集合中的数据项的处理工作分散到多个CPU上,以便并发处理多个数据项。
要让LINQ to Objects查询调用这些方法的并行版本,必须将顺序查询(基于IEnumerable或者IEnumerable<T>)转换成并行查询(基于ParallelQuery或者Paralle(Query<ID>),这是用ParallelEnumerable 的AsParallel扩展方法来实现的".
public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source) public static ParallelQuery AsParallel(this IEnumerable source)
private static void ObsoleteMethods(Assembly assembly) { var query = from type in assembly.GetExportedTypes().AsParallel() from method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) let obsoleteAttrType = typeof(ObsoleteAttribute) where Attribute.IsDefined(method, obsoleteAttrType) orderby type.FullName let obsoleteAttrObj = (ObsoleteAttribute) Attribute.GetCustomAttribute(method, obsoleteAttrType) select String.Format("Type={0}\nMethod={1}\nMessage={2}\n", type.FullName, method.ToString(), obsoleteAttrObj.Message); // Display the results foreach (var result in query) Console.WriteLine(result); }
虽然不太常见,但在一个查询中,可以从执行并行操作切换回执行顺序操作,这是通过调用ParallelEnumerable的AsSeonential方法做到的
public static IEnumerable<TSource> AsSequential<TSource>(this ParallelQuery<TSource> source)
通常,一个LINQ查询的结果数据是让某个线程执行一个foreach语句来计算获得的,就像前面展示的那样。这意味着只有一个线程遍历查询的所有结果。如果希望以并行方式处理查询的结果,就应该使用ParallelEnumerable的ForAll方法处理查询.
static void ForAll<TSource>(this ParallelQuery<TSource> source, Action<TSource> action)
让多个线程同时调用Console.WriteLine反而会损害性能,因为Console类内部会对线程进行同步,确保每次只有一个线程能访问控制台窗口,避免来自多个线程的文本在最后显示时乱成一团。希望为每个结果都执行计算时,才使用ForAll方法。
由于PLINQ用多个线程处理数据项,所以数据项被并发处理,结果被无序地返回。如果需要让PLINQ保持数据项的顺序,可调用ParallelEnumerable的AsOrdered方法。调用这个方法时,线程会成组处理数据项。然后,这些组被合并回去,同时保持顺序。这样会损害性能。以下操作符生成不排序的操作:Distinct,Except,Intersect,Union,Join,GroupBy,GroupJoin和ToLookup。在这些操作符之后要再次强制排序,只需调用AsOrdered方法。
PLINQ提供了一些额外的ParallelEnumerable方法,可调用它们来控制查询的处理方式:
public static ParallelQuery<TSource> WithCancellation<TSource>( this ParallelQuery<TSource> source, CancellationTokencancellationToken) public static ParallelQuery<TSource> WithDegreeOfParallelism<TSource>( this ParallelQuery<TSource> source, Int32degreeOfParallelism) public static ParallelQuery<TSource> WithExecutionMode<TSource>( this ParallelQuery<TSource> source, ParallelExecutionModeexecutionMode) public static ParallelQuery<TSource> WithMergeOptions<TSource>( this ParallelQuery<TSource> source, ParallelMergeOptionsmergeOptions)
如果查询要执行同步I/O操作,WithDegreeOParallelism还可传递比内核数大的一个数字,因为线程会在这些操作期间阻塞。这虽然会浪费更多线程,但可以用更少的时间生成最终结果。同步I/O操作在客户端应用程序中没什么问题,但我强烈建议不要在服务器应用程序中执行同步I/O操作。
PLINQ分析一个查询,然后决定如何最好地处理它。有的时候,顺序处理一个查询可以获得更好的性能,尤其是在使用以下任何操作时:Concat,ElementAt(OrDefault),First(orDefault),Last(OrDefault),Skip(While),Take(While)或Zip。使用Select(Many)或Where的重载版本,并向你的selector或predicate委托传递一个位置索引时也是如此。然而,可以调WithExecutionMode,向它传递某个ParallelExecutionMode标志,从而强迫查询以并行方式处理。
public enum ParallelExecutionMode { Default = 0, // Let Parallel LINQ decide to best process the query ForceParallelism = 1 // Force the query to be processed in parallel }
如前所述,并行LINQ让多个线程处理数据项,结果必须再合并回去。可调用WithMergeOptions,向它传递某个ParallelMergeOptions标志,从而控制这些结果的缓冲与合并方式。
public enum ParallelMergeOptions { Default = 0, // Same as AutoBuffered today (could change in the future) NotBuffered = 1, // Results are processed as ready AutoBuffered = 2, // Each thread buffers some results before processed FullyBuffered = 3 // Each thread buffers all results before processed }
这些选项使你能在某种程度上平衡执行速度和内存消耗。NotBuffered最省内存,但处理速变慢一些,FullyBuffered消费较多的内存,但运行得最快。AutoBuffered介于NotBuffered和 FullyBuffered之间。
27.8 执行定时计算限制操作
System.Threading命名空间定义了一个Timer类,可用它让一个线程池线程定时调用一个方法。构造Timer类的实例相当于告诉线程池:在将来某个时间(具体由你指定)回调你的一个方法。Timer类提供了几个相似的构造器:
public sealed class Timer : MarshalByRefObject, IDisposable { public Timer(TimerCallback callback, Object state, Int32 dueTime, Int32 period); public Timer(TimerCallback callback, Object state, UInt32 dueTime, UInt32 period); public Timer(TimerCallback callback, Object state, Int64 dueTime, Int64 period); public Timer(TimerCallback callback, Object state, Timespan dueTime, TimeSpan period); }
构造器的state参数允许在每次调用回调方法时都向它传递状态数据;如果没有需要传递的状态数据,可以传递null.dueTime参数告诉CLR在首次调用回调方法之前要等待多少毫秒。可以使用一个有符号或无符号的32位值、一个有符号的64位值或者一个TimeSpan值指定毫秒数。如果希望回调方法立即调用,为dueTime参数指定0即可。最后一个参数(period)指定了以后每次调用回调方法之前要等待多少毫秒。如果为这个参数传递Timeout.Infinite(-1),线程池线程只调用回调方法一次。
在内部,线程池为所有Timer对象只使用了一个线程。这个线程知道下一个Timer对象在什么时候到期(计时器还有多久触发)。下一个Timer对象到期时,线程就会唤醒,在内部调用ThreadPool的QueueUserWorkItem,将一个工作项添加到线程池的队列中,使你的回调方法得到调用。
如果回调方法的执行时间很长,计时器可能(在上个回调还没有完成的时候)再次触发。这可能造成多个线程池线程同时执行你的回调方法。为解决这个问题,我的建议是:构造Timer时,为period参数指定Timeout.Infinite,这样,计时器就只触发一次。然后,在你的回调方法中,调用Change方法来指定一个新的dueTime,并再次为period参数指定Timeout.Infinite。
public sealed class Timer : MarshalByRefObject, IDisposable { public Boolean Change(Int32 dueTime, Int32 period); public Boolean Change(UInt32 dueTime, UInt32 period); public Boolean Change(Int64 dueTime, Int64 period); public Boolean Change(TimeSpan dueTime, TimeSpan period); }
Timer类还提供了一个Dispose方法,允许完全取消计时器,并可在当时处于pending状态的所有回调完成之后,向notifyobject参数标识的内核对象发出信号。
public sealed class Timer : MarshalByRefObject, IDisposable { public Boolean Dispose(); public Boolean Dispose(WaitHandle notifyObject); }
internal static class TimerDemo { private static Timer s_timer; public static void Main() { Console.WriteLine("Checking status every 2 seconds"); // Create the Timer ensuring that it never fires. This ensures that // s_timer refers to it BEFORE Status is invoked by a thread pool thread s_timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite); // Now that s_timer is assigned to, we can let the timer fire knowing // that calling Change in Status will not throw a NullReferenceException s_timer.Change(0, Timeout.Infinite); Console.ReadLine(); // Prevent the process from terminating } // This method's signature must match the TimerCallback delegate private static void Status(Object state) { // This method is executed by a thread pool thread Console.WriteLine("In Status at {0}", DateTime.Now); Thread.Sleep(1000); // Simulates other work (1 second) // Just before returning, have the Timer fire again in 2 seconds s_timer.Change(2000, Timeout.Infinite); // When this method returns, the thread goes back // to the pool and waits for another work item } }
如果有需要定时执行的操作,可利用Task的静态Delay方法和C#的async和await关键字(第28章讨论)来编码。下面重写了前面的代码。
internal static class DelayDemo { public static void Main() { Console.WriteLine("Checking status every 2 seconds"); Status(); Console.ReadLine(); // Prevent the process from terminating } // This method can take whatever parameters you desire private static async void Status() { while (true) { Console.WriteLine("Checking status at {0}", DateTime.Now); // Put code to check status here... // At end of loop, delay 2 seconds without blocking a thread await Task.Delay(2000); // await allows thread to return // After 2 seconds, some thread will continue after await to loop around } } }
FCL提供了几个计时器
System.Threading的Timer类
这是上一节讨论的计时器。要在一个线程池线程上执行定时的(周期性发生的)后合任务,它是最好的计时器。
System.Windows.Forms的Timer类
构造这个类的实例,相当于告诉Windows将一个计时器和调用线程关联(参见Win32 SetTimer函数)。所有这些工作都只由一个线程完成-设置计时器的线程保证就是执行回调方法的线程。还意味着计时器方法不会由多个线程并发执行。
System.Windows.Threading的DispatcherTimer类
这个类是System.Windows.Forms的Timer类在Silverlight和WPF应用程序中的等价物。
Windows.UI.Xaml的DispatcherTimer类
这个类是System.Windows.Forms的Timer类在Windows Store应用中的等价物
System.Timers的Timer类
这个计时器本质上是System.Threading的Timer类的包装类。
27.9 线程池如何管理线程
27.9.1 设置线程池限制
CLR允许开发人员设置线程池要创建的最大线程数。但实践证明,线程池永远都不应该设置线程数上限,因为可能发生饥饿或死锁。
由于存在饥饿和死锁问题,所以CLR团队一直都在稳步地增加线程池默认拥有的最大线程数。目前默认值是大约1000个线程。
System.Threading.ThreadPool类提供了几个静态方法,可调用它们设置和查询线程池的线程数:GetMaxThreads,SetMaxThreads,GetMinThreads,SetMinThreads和GetAvailableThreads。强烈建议不要调用上述任何方法。
27.9.2如何管理工作者线程
ThreadPool.QueueUserWorkItem方法和Timer类总是将工作项放到全局队列中。工作者线程采用一个先入先出(firstin-ist-out,FIFO)算法将工作项从这个队列中取出,并处理它门。
非工作者线程调度一个Task时,该Task被添加到全局队列。但每个工作者线程都有自己的本地队列。工作者线程调度一个Task时,该Task被添加到调用线程的本地队列。
工作者线程采用后入先出(LIFO)算法将任务从本地队列取出。
如果所有本地队列都变空,那么工作者线程会使用FIFO算法,从全局队列提取一个工作项(取得它的锁),如果全局队列也为空,工作者线程会进入睡眠状态,等待事情的发生。如果睡眠了太长时间,它会自己醒来,并销毁自身,允许系统回收线程使用的资源(内核对象、栈、TEB等)。
线程池会快速创建工作者线程,使工作者线程的数量等于传给ThreadPool的SetMinThreads方法的值。如果从不调用这个方法(也建议你永远不调用这个方法),那么默认值等于你的进程允许使用的CPU数量,这是由进程的affinity mask(关联掩码)决定的。
浙公网安备 33010602011771号