TaskContinuationOptions
[Flags,serializable] public enum TaskContinuationOptions( None=ooooo, //默认 //将当前任务生成的子任务,安排到全局任务,不直接安排到本地任务队列。 PreferFairness=ox0001, //提议TaskScheduler应尽可能地创建线程池线程 LongRunning=ox0002, //任务是之间是没有父子关系的,但是该枚举项可以实现任务之间的父子关系:将一个Task和它的父Task关联(稍后讨论) AttachedToParent=ox0004, //任务试图和这个父任务连接将抛出一个 InvalidOperationException
DenychildAttach=oxo008, //强迫子任务使用默认调度器而不是父任务的调度器 Hidescheduler=Ox0010 //除非前置任务(antecedent task)完成,否则禁止延续任务完成(取消) Lazycancellation=oxO020 //这个标志指出你希望由执行第一个任务的线程执行 //Continuewith任务。第一个任务完成后,调用 //Continuewith的线程接着执行ContinueWith任务 ExecuteSynchronously=0x80000, //这些标志指出在什么情况下运行Continuewith任务 NotOnRanToCompletion =0x10000, NotOnFaulted=Ox20000, NotOnCanceled=0x40000, //这些标志是以上三个标志的便利组合 OnlyonCanceled=NotOnRanToCompletion l NotOnFaulted, OnlyonFaulted= NotOnRanToCompletion I NotOnCanceled, OnlyonRanToCompletion = NotOnFaulted | NotonCanceled,
TaskContinuationOptions.AttachedToParent使用案例
一个任务创建的一个或多个 Task对象默认是*任务,它们与创建它们的任务无关。但 TaskCreationOptions.AttachedToParent
标志将一个Task和创建它的Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束。调用ContinueWith方法创建Task时,
可指定TaskContinuationOptions.AttachedToParent标志将延续任务指定成子任务(这句话看下面的例子就明白了)。----ckr via C# 第四版P625
/*只要在任务A中创建的任务(a、b、c、包括b的ContinueWith创建的任务) 都是*任务,只有当a、b、c等设定了AttachedToParent才是A的子任务。 * 具有 TaskCreationOptions.AttachedToParent或TaskContinuationOptions.AttachedToParent特性的任务都依附与它的父任务。 * 如果父任务的TaskCreationOptions不为DenyChildAttach,子任务的AttachedToParent就起作用。 * 以下实列父亲任务默认的TaskCreationOptions为None,因此子任务的AttachedToParent就起作用。 * 如果父任务已经完成,但是在依附与父任务的子任务还未完成 ,那么此时父任务就处于WaitingForChildrenToComplete。 * 只有当依附与父任务的子任务和父任务都完成时,父任务的状态才会变成RanToCompletion。 * RanToCompletion=【依附于父任务的子任务RanToCompletion】+【父任务RanToCompletion】 * * */ Task taskparent = new(() => { Task subtask1 = new(() => { Console.WriteLine("subtask1 不和taskparent关联,等subtask1结束后执行"); Console.WriteLine("subtask1 开始执行,睡觉0.1s "); Thread.Sleep(100); Console.WriteLine("subtask1 睡醒了 "); } ); //ContinueWith方式创建的子任务,它不依附于父任务taskparent,是个独立的任务,因此父任务不需要等待它完成。 Task subtask2 = subtask1.ContinueWith(task => { Console.WriteLine("subtask2 不和subtask1关联,等subtask1结束后执行"); Console.WriteLine("subtask2 开始执行,睡觉3s "); Thread.Sleep(3000); Console.WriteLine("subtask2 睡醒了 "); }); //ContinueWith方式创建子任务 Task subtask3 = subtask1.ContinueWith(task => { Console.WriteLine("subtask3 和subtask1关联,等subtask1结束后执行"); Console.WriteLine("subtask3 开始执行,睡觉3s "); Thread.Sleep(1000); Console.WriteLine("subtask3 睡醒了 ");
//它设置了依附于父任务taskparent(不是subtask1),如果父任务的TaskCreationOptions不为DenyChildAttach。
//那么他就起作用,父任务要等待它一起完成后,才会把状态修改为RanToCompletion,如果父任务提取完成,在等候的子任务期间父任务状态是WaitingForChildrenToComplete },TaskContinuationOptions.AttachedToParent); subtask1.Start(); Console.WriteLine(subtask1.CreationOptions); }); taskparent.Start(); while (!taskparent.IsCompleted) { Console.WriteLine(taskparent.Status); } Console.WriteLine(taskparent.Status); Console.Read();
TaskCreationOptions.DenyChildAttach
(拒收义子)拒绝任何子任务依附与它。所有它的子任务中设置的TaskCreationOptions.AttachedToParent或TaskContinuationOptions.AttachedToParent属性都对他无效。
它不会等待任何子任务,只要它自己的任务完成,它的状态就会变成RanToCompletion。
Task taskparent = new(() => { Task subtask1 = new(() => { Console.WriteLine("subtask1 不和taskparent关联,等subtask1结束后执行"); Console.WriteLine("subtask1 开始执行,睡觉0.1s "); Thread.Sleep(100); Console.WriteLine("subtask1 睡醒了 "); } ); Task subtask2 = subtask1.ContinueWith(task => { Console.WriteLine("subtask2 不和subtask1关联,等subtask1结束后执行"); Console.WriteLine("subtask2 开始执行,睡觉3s "); Thread.Sleep(3000); Console.WriteLine("subtask2 睡醒了 "); }); //ContinueWith方式创建的子任务,它依附于父任务taskparent(不是subtask1),如果父任务的TaskCreationOptions不为DenyChildAttach。那么他就起作用。 Task subtask3 = subtask1.ContinueWith(task => { Console.WriteLine("subtask3 和subtask1关联,等subtask1结束后执行"); Console.WriteLine("subtask3 开始执行,睡觉3s "); Thread.Sleep(1000); Console.WriteLine("subtask3 睡醒了 "); },TaskContinuationOptions.AttachedToParent); subtask1.Start(); Console.WriteLine(subtask1.CreationOptions); //不会等到任何子任务,及时子任务设置了TaskCreationOptions.AttachedToParent或TaskContinuationOptions.AttachedToParent属性 }, TaskCreationOptions.DenyChildAttach); taskparent.Start(); while (!taskparent.IsCompleted) { Console.WriteLine(taskparent.Status); } Console.WriteLine(taskparent.Status); Console.Read();
TaskContinuationOptions.Hidescheduler
强迫子任务使用默认调度器而不是父任务或第一个任务的调度器
除了Task.Run()模式使用的默认的线程池调度器,task.start()和TaskFactory.StartNew()都使用Current TaskScheduler。所以在任务内创建子任务会继承上一级任务的任务调度器。
Task taskparent = new(() => { //第一次嵌套 Task Employer1 = new(() => { Console.WriteLine($"使用父任务的调度器 "); Console.WriteLine($"Employer1 Current TaskScheduler:{TaskScheduler.Current}"); }); // 拒绝使用父类的Scheduler调度器,使用默认的线程池任务调度器ThreadPoolTaskScheduler Task Employer3 = Employer1.ContinueWith(task => { Console.WriteLine($"拒绝使用父任务的调度器,使用默认的线程池任务调度器"); Console.WriteLine($"Employer3 Current TaskScheduler:{TaskScheduler.Current}"); //第二次嵌套 Task Employer2 = new(() => { Console.WriteLine($"使用父任务的调度器 "); Console.WriteLine($"Employer2 Current TaskScheduler:{TaskScheduler.Current}"); }); Employer2.Start(); }, TaskContinuationOptions.HideScheduler); Employer1.Start(); }); taskparent.Start(new PerThreadTaskScheduler()); Console.WriteLine(taskparent.Status); Console.Read(); /*输出 * WaitingToRun 使用父任务的调度器 Employer1 Current TaskScheduler:PerThreadTaskScheduler 拒绝使用父任务的调度器,使用默认的线程池任务调度器 Employer3 Current TaskScheduler:System.Threading.Tasks.ThreadPoolTaskScheduler 使用父任务的调度器 Employer2 Current TaskScheduler:System.Threading.Tasks.ThreadPoolTaskScheduler */
PerThreadTaskScheduler类
public class PerThreadTaskScheduler : TaskScheduler { protected override IEnumerable<Task> GetScheduledTasks() { return null; } protected override void QueueTask(Task task) { var thread = new Thread(() => { TryExecuteTask(task); }); thread.Start(); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { throw new NotImplementedException(); } }View Code
TaskContinuationOptions.ExecuteSynchronously 使用案例
ContinueWith和第一个任务线程同一个线程。
using System.Reflection; Task taskParent = new(() => { Console.WriteLine($"taskParent CurrentId is {Task.CurrentId} And Thread{Environment.CurrentManagedThreadId}"); }); Task tasktest = taskParent.ContinueWith(tas => { Console.WriteLine($"taskParent Status is : {Enum.GetName(taskParent.Status)} and TaskScheduler is {TaskScheduler.Current} "); Console.WriteLine($"Continue task CurrentId is {Task.CurrentId} And Thread{Environment.CurrentManagedThreadId}"); }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion ); taskParent.Start(); Console.Read(); /* 输出: taskParent CurrentId is 4 And Thread6 taskParent Status is : RanToCompletion and TaskScheduler is System.Threading.Tasks.ThreadPoolTaskScheduler Continue task CurrentId is 3 And Thread6 */
其他枚举用法:
//这些标志指出在什么情况下运行Continuewith任务 NotOnRanToCompletion =0x10000, NotOnFaulted=Ox20000, NotOnCanceled=0x40000, //这些标志是以上三个标志的便利组合 OnlyonCanceled=NotOnRanToCompletion l NotOnFaulted, OnlyonFaulted= NotOnRanToCompletion I NotOnCanceled, OnlyonRanToCompletion = NotOnFaulted | NotonCanceled,
这些枚举的用法都一样,该案例用了OnlyonCanceled和OnlyonRanToCompletion
CancellationTokenSource cts = new CancellationTokenSource(); cts.Token.Register(() =>Console.WriteLine($"临时请假")); Task taskparent = new(() => { Task Employer1 = new(() => { while (!cts.Token.IsCancellationRequested) { Thread.Sleep(2000); try { cts.Token.ThrowIfCancellationRequested(); } finally { } } //传入取消令牌,执行continue任务 时候要调用该令牌 }, cts.Token); Task Employer2 = Employer1.ContinueWith(task => { //nameof(Employer1) 防止变量名修改时候 忘记修改字符总的字符窜了 Console.WriteLine($"{nameof(Employer1)} 请假了,{nameof(Employer2)}代替{nameof(Employer1)}工作"); Console.WriteLine("Employer2 开始 工作 "); Thread.Sleep(3000); Console.WriteLine($"{nameof(Employer2)}完成了剩下的工作 "); // 只有当Employer1任务 取消时候 ,Employer2任务才开始运行 }, TaskContinuationOptions.OnlyOnCanceled); Task Employer3 = Employer1.ContinueWith(task => { Thread.Sleep(2000); Console.WriteLine("Employer3 睡醒了 "); // 只有当Employer1完成时, 该任务才开始运行 }, TaskContinuationOptions.AttachedToParent|TaskContinuationOptions.OnlyOnRanToCompletion); Employer1.Start(); cts.Cancel(); } ); taskparent.Start(); Console.WriteLine(taskparent.Status); Console.Read(); /*输出 * 临时请假 WaitingToRun 临时请假 Employer1 请假了,Employer2代替Employer1工作 Employer2 开始 工作 Employer2完成了剩下的工作*/
TaskContinuationOptions.LongRunning使用案例
启用一个后台线程,不属于线程池线程。
TaskFactory LongTask = new TaskFactory( TaskCreationOptions.LongRunning,TaskContinuationOptions.AttachedToParent); TaskFactory preferTask = new TaskFactory( TaskCreationOptions.PreferFairness, TaskContinuationOptions.AttachedToParent); LongTask.StartNew(() => { Console.WriteLine("Thread.CurrentThread.IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread); Console.WriteLine("Thread.CurrentThread.IsBackground:" + Thread.CurrentThread.IsBackground); }); Console.ReadKey(); /* Thread.CurrentThread.IsThreadPoolThread:False Thread.CurrentThread.IsBackground:True */
TaskContinuationOptions.PreferFairness
任务并行库实现良好性能的方法之一是通过"工作窃取"。.NET 4 线程池支持工作窃取,以便通过任务并行库及其默认计划程序进行访问。这表现为线程池中的每个线程都有自己的工作队列;当该线程创建任务时,默认情况下,这些任务将排队到线程的本地队列中,而不是排队到对 ThreadPool.QueueUserWorkItem 的调用通常面向的全局队列中。当线程搜索要执行的工作时,它会从其本地队列开始,该操作由于改进了缓存局部性,最小化了争用等,从而实现了一些额外的效率。但是,这种逻辑也会影响公平性。
典型的线程池将具有单个队列,用于维护要执行的所有工作。当池中的线程准备好处理另一个工作项时,它们将从队列的头部取消排队工作,当新工作到达池中执行时,它将排队到队列的尾部。这为工作项之间提供了一定程度的公平性,因为首先到达的工作项更有可能被选中并首先开始执行。
偷工作扰乱了这种公平。池外部的线程可能正在排队工作,但如果池中的线程也在生成工作,则池生成的工作将优先于其他工作项,具体取决于池中线程(这些线程首先开始使用其本地队列搜索工作, 仅继续进入全局队列,然后继续到其他线程的队列(如果本地没有工作可用)。这种行为通常是预期的,甚至是期望的,因为如果正在执行的工作项正在生成更多工作,则生成的工作通常被视为正在处理的整体操作的一部分,因此它比其他不相关的工作更可取是有道理的。例如,想象一个快速排序操作,其中每个递归排序调用都可能导致几个进一步的递归调用;这些调用(在并行实现中可能是单个任务)是全系列排序操作的一部分。
不过,在某些情况下,这种默认行为是不合适的,其中应该在池中的线程生成的特定工作项和其他线程生成的工作项之间保持公平性。对于长链的延续,通常就是这种情况,其中生成的工作不被视为当前工作的一部分,而是当前工作的后续工作。在这些情况下,您可能希望以公平的方式将后续工作与系统中的其他工作放在一起。这就是TaskCreationOptions.PreferFairness可以证明有用的地方。
将 Task 调度到默认调度程序时,调度程序将查看任务从中排队的当前线程是否是具有自己的本地队列的 ThreadPool 线程。如果不是,则工作项将排队到全局队列。如果是,计划程序还将检查任务的 TaskCreationOptions 值是否包含"首选公平性"标志,默认情况下该标志未打开。如果设置了该标志,即使线程确实有自己的本地队列,调度程序仍将 Task 排队到全局队列,而不是本地队列。通过这种方式,该任务将与全局排队的所有其他工作项一起被公平地考虑。
刚才描述的是默认计划程序中优先公平标志的当前实现。实现当然可以更改,但不会更改的是标志的目的:通过指定 PreferFairness,您可以告诉系统不应仅仅因为此任务来自本地队列而对其进行优先级排序。您是在告诉系统,您希望系统尽最大努力确保此任务以先到先得的方式进行优先级排序。
另一件需要注意的事情是,Task本身对这面旗帜一无所知。它只是一个标志,设置为任务上的一个选项。调度程序决定了它想要如何处理这个特定的选项,就像TaskCreationOptions.LongRunning一样。默认调度程序按上述方式处理它,但另一个调度程序(例如您编写的调度程序)可以根据需要使用此标志,包括忽略它。因此,命名"首选"而不是像"保证"这样更严格的东西。