计算限制的异步操作
1,CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列
2,每CLR一个线程池,这个线程池有CLR控制的所有AppDomain共享。如果一个进程加载了多个CLR,那么每个CLR都有它自己的线程池
3,要将一个异步的计算限制操作放到线程池的队列中,通常使用ThreadPool类定义的以下方法之一
public static bool QueueUserWorkItem(WaitCallback callBack); public static bool QueueUserWorkItem(WaitCallback callBack, object state);
这些方法向线程池的队列中加一个“工作项”(callBack)以及可选的状态数据。然后,所有方法会立即返回。无state参数的那个版本向回调方法传递一个null
static void Main(string[] args) { ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); } private static void ComputeBoundOp(Object state) { Console.WriteLine(state);
//这个方法返回后,线程回到池中,等待另一个任务 }
1,每个线程都关联一个执行上下文数据结构。
2,执行上下文包括
①安全设施(压缩栈、Thread的Principal、Windwos身份)
②宿主设置(参见System.Threading.HostExecutionContextManager)
③逻辑调用上下文数据(System.Runtime.Remoting.Messaging.CallContext的LogicalGetData和LogicalSetData方法)
3,每当一个线程(初始线程)使用另一个线程(辅助线性)执行任务时,前者的执行上下文应该流向(复制到)辅助线性。这会对性能造成影响。
4,System.Threading.ExecutionContext类控制线程的执行上下文如果从一个线程“流”向另一个
public sealed class ExecutionContext : IDisposable, ISerializable { [SecurityCritical] public static AsyncFlowControl SuppressFlow();//阻止流 public static void RestoreFlow();//恢复流 public static Boolean IsFlowSuppressed();//是否阻止流 }
static void Main(string[] args) { //将一些数据方法Main线程的逻辑调用上下文中 CallContext.LogicalSetData("Name", "Hunter"); //执行上下文流向辅助线程,辅助线程可以访问逻辑调用上下文的数据 ThreadPool.QueueUserWorkItem(r => Console.WriteLine(CallContext.LogicalGetData("Name"))); //现在,阻止Main线程的执行上下文 ExecutionContext.SuppressFlow(); //辅助线程不能访问逻辑调用上下文的数据 ThreadPool.QueueUserWorkItem(r => Console.WriteLine(CallContext.LogicalGetData("Name"))); //恢复Main线程的执行上下文的流动,以免将来使用更多的线程池线程 ExecutionContext.RestoreFlow(); ThreadPool.QueueUserWorkItem(r => Console.WriteLine(CallContext.LogicalGetData("Name"))); //输出: //Hunter // //Hunter Console.ReadLine(); }
1,CancellationTokenSource
static void Main(string[] args) { //取消操作首先创建此对象 CancellationTokenSource cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(r => Count(cts.Token)); Thread.Sleep(2000); //如果Count方法已返回,Cancel没有任何效果 cts.Cancel();//取消操作 //输出: //0 //1 //2 //取消了 Console.ReadLine(); } private static void Count(CancellationToken token) { for (int i = 0; i < 10; i++) { Console.WriteLine(i); if (token.IsCancellationRequested)//是否取消了 { Console.WriteLine("取消了"); break; } Thread.Sleep(1000); } }
要执行一个不允许被取消的操作,可向该操作传递通过调用CancellationToken.None属性而返回的CancellationToken,该属性返回的CancellationToken不和任何CancellationTokenSource对象关联(实例的私有字段为null)
2,取消回调
static void Main(string[] args) { //取消操作首先创建此对象 CancellationTokenSource cts = new CancellationTokenSource(); cts.Token.Register(() => Console.WriteLine("取消回调1")); cts.Token.Register(() => Console.WriteLine("取消回调2")); cts.Cancel();//取消操作 //输出: //取消回调2 //取消回调1 Console.ReadLine(); }
①useSynchronizationContext参数:
false:调用Cancel的线程会顺序调用已登记的所有方法
true:回调方法会被send给已捕捉的useSynchronizationContext对象
②CancellationTokenSource的Cancel方法参数
true:抛出了未处理异常的第一个方法会阻止其他回调方法的执行,抛出的异常会从Cancel中抛出
false:所有的回调方法都会执行。所有未处理的异常会被添加到一个集合中,抛出的异常会从Cancel中抛出(AggregateException)
3,向一个CancellationTokenSource登记两个回调
static void Main(string[] args) { //创建一个CancellationTokenSource var cts1 =new CancellationTokenSource(); cts1.Token.Register(() => Console.WriteLine("cts1 取消了")); //创建另一个创建一个CancellationTokenSource var cts2 = new CancellationTokenSource(); cts2.Token.Register(() => Console.WriteLine("cts2 取消了")); //创一个新的CancellationTokenSource,它在cts1或cts2取消时取消 var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); linkedCts.Token.Register(() => Console.WriteLine("linkedCts 取消了")); cts2.Cancel(); Console.WriteLine(cts1.IsCancellationRequested+" "+cts2.IsCancellationRequested+" "+linkedCts.IsCancellationRequested); //输出: //linkedCts 取消了 //cts2 取消了 //Flase True True Console.ReadLine(); }
4,超时取消
static void Main(string[] args) { //创建另一个创建一个CancellationTokenSource var cts2 = new CancellationTokenSource(); cts2.Token.Register(() => { Console.WriteLine("cts2 取消了"); }); cts2.CancelAfter(1000);//如果1秒钟还没有执行到cts2.Cancel()取消,则自动取消 Thread.Sleep(100000); cts2.Cancel(); //1秒中之后程序回输出:cts2 取消了 Console.ReadLine(); }
ThreadPool.QueueUserWorkItem()没有内建机制让你知道操作在什么时候完成,也没有机制在完成操作时获得返回值
ThreadPool.QueueUserWorkItem(r => { }); //调用QueueUserWorkItem new Task(r => { Console.WriteLine(r); }, 5).Start(); //用Task来做相同的事情。输出5 Task.Run(() => { }); //另一个等价写法
可选择向构造器传递一些TaskCreationOptions标志来控制Task的执行方式
[Flags,Serializable] public enum TaskCreationOptions { //默认 None = 0x0000, //提议TaskScheduler你希望该任务尽快执行(给你的感觉就像queue的感觉) PreRunning = 0x0001, //提议TaskScheduler应尽可能地创建线程池线程(如果是长时间运行的任务,建议使用此选项,使用此选项会创建一个线程,还不是使用线程池) LongRunning = 0x0002, //该提议总是被采纳:将一个Task和它的父Task关联 AttachedToParent=0x0004, //该提议总是被采纳:如果一个任务视图和这个父任务链接,它就是一个普通任务,而不是子任务 DenyChildAttach = 0x0008, //该提议总是被采纳:强迫子任务使用默认调度器而不是父任务的调度器 HideScheduler = 0x0010, }
1,等待任务完成并获取结果
static void Main(string[] args) { //创建一个任务,现在还没有开始运行 Task<int> t1 = new Task<int>(() => 1); //可以后再启动任务 t1.Start(); //可选择显示等待任务完成 t1.Wait(); Console.WriteLine(t1.Result);//输出:1 Console.ReadLine(); }
任务抛出异常会被吞噬并存储到一个集合中,而线程池线程可以返回到线程池中。调用Wait方法或者Result属性时,这些成员会抛出一个System.AggregateException对象
除了等待单个任务,还可以等待一个Task对象数组
//会阻塞调用线程,直到数组中的任何Task对象完成 //方法返回Int32数组索引值,指明完成的是哪个Task对象 //方法返回后。线程被唤醒并继续运行 //如果发生超时,方法返回-1 //如果WaitAny通过一个CancellationToken取消,会抛出一个OperationCanceledException Task.WaitAny(task1, task2); //会阻塞调用线程,直到数组中的所有Task对象完成 //所有Task对象都完成,WaitAll返回true //如果发生超时,方法返回false //如果WaitAll通过一个CancellationToken取消,会抛出一个OperationCanceledException Task.WaitAll(task1, task2);
2,取消任务
static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task<Int32> t = Task.Run(() => Sum(cts.Token, 100), cts.Token); //在之后的某个时间,取消CancellationTokenSource以取消Task cts.Cancel(); try { //如果任务已取消,Result会抛出一个AggregateException异常 Console.WriteLine(t.Result); } catch (AggregateException x) { //将任何OperationCanceledException对象都视为已处理 //其他任何异常都造成抛出一个新的AggregateException //其中只包含未处理的异常 x.Handle(e => e is OperationCanceledException); //所有异常都处理好之后,执行下面这一行 Console.WriteLine("计算已取消"); } Console.ReadLine(); } private static Int32 Sum(CancellationToken token, Int32 n) { int num = 0; for (; n > 0; n--) { //如果任务取消,则抛出OperationCanceledException异常 //这样设计是因为Task会捕获异常 token.ThrowIfCancellationRequested(); checked { num += n; } } return num; }
4,任务完成时自动启动新任务
TaskContinuationOptions.OnlyOnCanceled//第一个任务被取消时才执行
TaskContinuationOptions.OnlyOnFaulted//第一个任务被抛异常时才执行
TaskContinuationOptions.OnlyOnRanToCompletion//第一个任务顺利完成时才执行
[Flags, Serializable] public enum TaskContinuationOptions { //默认 None = 0x0000, //提议TaskScheduler你希望该任务尽快执行 PreferRunning = 0x0001, //提议TaskScheduler应尽可能地创建线程池线程 LongRunning = 0x0002, //AttachedToParent:将一个Task和它的父Task关联 AttachedToParent = 0x0004, //该提议总是被采纳:如果一个任务视图和这个父任务链接,它就是一个普通任务,而不是子任务 DenyChildAttach = 0x0008, //该提议总是被采纳:强迫子任务使用默认调度器而不是父任务的调度器 HideScheduler = 0x0010, //除非前置任务(antecedent task)完成,否则精致延续任务完成(取消) LazyCancellation = 0x0020, //这个标志指出你希望由执行第一个任务的线程执行ContinueWith任务。 //第一个任务完成后,调用ContinueWith的线程接着执行ContinueWith任务 ExecuteSynchronously = 0x80000, //这些标志指出在什么情况下运行ContinueWith任务 NotOnRanToCompletion = 0x10000, NotOnFaulted = 0x20000, NotOnCanceled = 0x40000, //这些标志是以上三个标志的遍历组合 OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted,//第一个任务被取消时才执行 OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled,//第一个任务被抛异常时才执行 OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled//第一个任务顺利完成时才执行 }
static void Main(string[] args) { var t1 = Task.Run(() =>Console.WriteLine("第一个任务")); t1.ContinueWith(task => Console.WriteLine("任务顺利完成时执行"), TaskContinuationOptions.OnlyOnRanToCompletion); t1.ContinueWith(task => Console.WriteLine("任务抛异常时执行"), TaskContinuationOptions.OnlyOnFaulted); t1.ContinueWith(task => Console.WriteLine("任务取消时执行"), TaskContinuationOptions.OnlyOnCanceled); //输出: //第一个任务 //任务顺利完成时执行 Console.ReadLine(); }
5,任务可以启动子任务
static void Main(string[] args) { //提高性能,一个任务泵本需要3秒执行完,使用子任务只需要1秒多就可以执行完 Task<Int32[]> parent = new Task<Int32[]>(() => { var results = new int[3]; //创建并启动3个子任务 new Task(() => { Thread.Sleep(1000); results[0] = 1; }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(1000); results[1] = 2; }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(1000); results[2] = 3; }, TaskCreationOptions.AttachedToParent).Start(); return results; }); //父任务及其子任务运行完成后,用一个延续任务显示结果 parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine)); //启动父任务,便于它启动它的子任务 parent.Start(); Console.ReadLine(); }
TaskCreationOptions.AttachedToParent标志将一个Task和创建它的Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束
6,任务内部揭秘
①每个Task对象都有一组字段。必须为这些字段分配内存
Int32 ID(只读唯一标识,首次查询此字段时分配)
代表Task执行状态的一个Int32
对父任务的引用
对Task创建时指定的TaskScheduler的引用
对回调方法的引用
对要传给回调方法的对象的引用(可通过Task的只读AsyncState属性查询)
对ExecutionContext的引用
对ManualResetEventSlim对象的引用
②Task生存期状态
//这些标志指出一个Task在其生命周期内的状态 public enum TaskStatus { Created,//任务以显示创建;可以手动Start()这个任务 WaitingForActivation,//任务以隐式创建;会自动开始 WaitingToRun,//任务以调度,但尚未运行 Running,//任务正在运行 WaitingForChildrenToComplate,//任务正在等待它的子任务完成,子任务完成后它才完成 //任务最重状态时以下三个之一 RanToComplation,//运行完成 Canceled,//取消 Faulted//出错 }
首次构造Task对象时,状态为Created
当任务启动时,它的状态变成WaitingToRun
Task实际在一个线程上运行时,它的状态变成Runting
任务停止运行并等待它的任何子任务时,状态变成WaitingForChildrenToComlate
任务完成时变成以下状态之一:RanToComplate(运行完成)、Canceled(取消)、Faulted(出错)
调用ContinueWith、ContinueWhenAll、ContinueWhenAny或FromAsync等方法创建的Task对象出于WaitingForActivation状态
通过TaskComplationSource<TResult>对象创建的Task也出于WaitingForActivation状态
7,任务工厂
为什么要使用任务工厂:有时需要创建一组共享相同配置的Task对象,为避免机械地将相同的参数传给每个Task的构造器
static void Main(string[] args) { Task parent = new Task(() => { var cts = new CancellationTokenSource(); var tf=new TaskFactory<Int32>( cts.Token,//每个task都共享相同的CancellationTokenSource标记 TaskCreationOptions.AttachedToParent, //任务都被视为父任务的子任务 TaskContinuationOptions.ExecuteSynchronously, //TaskFactory创建的所有延续任务都以同步方式执行 TaskScheduler.Default//使用默认的任务调度器 ); //这个任务创建并启动3个子任务 var childTasks = new[] { tf.StartNew(() => 1), tf.StartNew(() => 2), tf.StartNew(() => { throw new ArgumentNullException();}) }; //任何子任务抛出异常,则终止所有子任务 foreach (var childTask in childTasks) { childTask.ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); } //输出子任务值合计 tf.ContinueWhenAll(childTasks, //该子任务不受 取消 影响 complatedTasks => complatedTasks.Where(r => !r.IsFaulted && !r.IsCanceled).Sum(r => r.Result),CancellationToken.None) .ContinueWith(r => Console.WriteLine(r.Result), TaskContinuationOptions.ExecuteSynchronously); }); //上个任务有异常时执行输出抛出异常的类型 parent.ContinueWith(p => { StringBuilder sb = new StringBuilder(); foreach (var e in p.Exception.Flatten().InnerExceptions) { sb.Append(e.GetType().ToString() + Environment.NewLine); } Console.WriteLine(sb.ToString()); },TaskContinuationOptions.OnlyOnFaulted); parent.Start(); Console.ReadLine(); }
8,任务调度器
FCL提供了两个派生自TaskScheduler的类型:
①线程池任务调度器(默认使用)
②同步上下文任务调度器
同步上下文任务调度器提供了图形用户界面的应用程序,例如Windows窗体等。它将所有任务都调度给应用程序的GUI线程,是所有任务代码都能成功更新UI组件(按钮、菜单项等)
private void Form1_Load(object sender, EventArgs e) { //TaskScheduler.FromCurrentSynchronizationContext()使用同步上下文任务调度器 Task.Run(() => { label1.Text = "同步上下文任务调度器(失败)"; }, CancellationToken.None) .ContinueWith(task => { label1.Text = "同步上下文任务调度器(成功)"; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext()); }
8,Parallel的静态For,ForEach,Invoke方法
1>使用Parallel的前提条件:
①工作项必须能并行执行
②避免修改共享数据的工作项
2>Parallel的性能开销:
①委托对象必须分配,而针对每个工作项都要调用一次这些委托
②如果为处理非常快的工作项使用Parallel的方法,则会得不偿失
static void Main(string[] args) { //最后输出Complate。线程要在所有工作完成后才继续运行。如果存在异常,则抛出AggregateException //使用For执行更快 Parallel.For(0, 100, r => Console.WriteLine(r)); Parallel.ForEach(new[] {1, 2}, r => { Console.WriteLine(r); }); Parallel.Invoke(()=>Method1(), () => Method2()); Console.WriteLine("Complate"); Console.ReadLine(); }
3>Parallel的静态For,ForEach,Invoke方法都提供了此参数
public class ParallelOptions { public ParallelOptions(); //允许取消操作(默认为CancellationToken.None) public CancellationToken CancellationToken { get; set; } //允许指定要使用哪个TaskScheduler(默认为TaskScheduler.Default) public int MaxDegreeOfParallelism { get; set; } }
4>Parallel的静态For,ForEach方法有一些重载版本允许传递3个委托
①任务局部初始化委托(localInit):为参与工作的每个任务都调用一次改委托。这个委托是在任务被要求处理一个工作项之前调用的
②主体委托(body):为参与工作的各个线程所处理的每一项都调用一次该委托
③任务局部终结委托(localFinally):为参与工作的每一个任务都调用一次改委托。这个委托实在任务处理好派发给它的所有工作项之后调用的。即使主体委托代码引发一个未处理的异常,也会调用它
static void Main(string[] args) { Console.WriteLine(DirectoryBytes(@"E:\web", "*", SearchOption.TopDirectoryOnly)); Console.ReadLine(); } private static long DirectoryBytes(string path, string searchPattern, SearchOption searchOption) { var files = Directory.EnumerateFiles(path, searchPattern, searchOption); long masterTotal = 0; ParallelLoopResult result = Parallel.ForEach<string, long>(files, () => { //localInit:每个任务开始之前调用一次 //每个任务开始之前,总计值都初始化为0 return 0; }, (file, loopState, index, taskLocalTotal) =>//taskLocalTotal接受localInit的返回值 { //body:每个工作项调用一次 //获得这个文件的大小,把它添加到这个任务的累加值上 long fileLength = 0; FileStream fs = null; try { fs = File.OpenRead(file); fileLength = fs.Length; } catch (IOException) { //忽略拒绝访问的文件 } finally { if (fs != null) fs.Dispose(); } return taskLocalTotal + fileLength; }, taskLocalTotal =>//taskLocalTotal接受body的返回值 { //localFinally:每个任务完成时调用一次 //将这个任务的总计值(taskLocalTotal)加到总的总计值(masterTotal)上 Interlocked.Add(ref masterTotal, taskLocalTotal); }); return masterTotal; } }
loopState:
public class ParallelLoopState { //告诉循环停止处理任何跟多的工作 public void Stop(); //停止之后该属性为true public bool IsStopped { get; } //告诉循环不再继续处理当前项之后的项 public void Break(); //该属性返回在处理过程中调用过Break方法的最低的项(从来没有调用过,则返回null) public Int64? LowestBreakInteration { get; } //任何项造成未处理的异常,则为true public bool IsException { get; } //调用过Stop、Break、取消过CancellationTokenSource,造成未处理异常。则为true public bool ShouldExitCurrentInteration { get; } }
ParallelLoopResult:
public struct ParallelLoopResult { //如果操作提前终止,则返回false public bool IsComplated { get; } public long? LowestBreakInteration { get; } }
IsComplated=true 循环运行完成,所有项得到了处理
IsComplated=false LowestBreakInteration=null 参与工作的某个线程调用了Stop方法
IsComplated=false LowestBreakInteration!=null参与工作的某个线程调用了Break方法
public class Program { [Obsolete("已经过时")] public static void Main(string[] args) { //AsParallel将顺序查询转换成并行查询 //AsSequential将并行操作转换为循序操作 var query = from type in Assembly.GetExecutingAssembly().GetExportedTypes().AsParallel().AsSequential().AsParallel() from methed in type.GetMethods() let obsoleteAttrType=typeof(ObsoleteAttribute) where Attribute.IsDefined(methed, obsoleteAttrType) orderby type.FullName let obsoleteAttrObj=(ObsoleteAttribute)Attribute.GetCustomAttribute(methed,obsoleteAttrType) select $"Type={type.FullName};Methed={methed.Name};Message={obsoleteAttrObj.Message}"; //并行处理结果 query.ForAll(Console.WriteLine); Console.ReadLine(); } }
PLINQ处理是乱序的,如果想保持顺序,则可调用ParallelEnumerable的AsOrdered方法,调用这个方法线程会组成处理数据项,然后,这些组被合并回去,同时保持顺序(会损害性能)
PLINQ主要是划分区块,然后对区块(不同的线程)进行聚合计算,从而达到分而治之
AsParallel():将串行的代码转换为并行
AsOrdered():还原集合的初始顺序
AsOrdered和OrderBy比较
例如集合[10,1,4,2]
AsOrdered=>[10,1,4,2]
OrderBy=>[1,2,4,10]
AsSequential():将并行转换为串行
1,构造Timer参数:
callback:回调方法
state:回调方法参数
dueTime:首次回调方法之前要等待多时毫秒(0为立即执行)
period:以后每次回调方法之前要等待多时毫秒(Timeout.Infinite线程池只调用回调方法一次)
使用Timer对象时,要确定一个变量在保持Timer对象的存活,否则对你的回调方法的调用就会停止
2,演示如何让一个线程池线程立即调用回调方法
class Program { private static Timer _timer; static void Main(string[] args) { //创建但不启动计时器 _timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite); //立即启动一次计时器 _timer.Change(0, Timeout.Infinite); Console.ReadLine();//防止进程终止 } private static void Status(object state) { Console.WriteLine("进入Status方法"); //这个方法由一个线程池线程执行 Thread.Sleep(1000);//模拟其它工作(1秒) //返回前让Timer在2秒后再次触发一次 _timer.Change(2000, Timeout.Infinite); //这个方法返回后,线程池回归池中,等待下一个工作项 } }
利用Task的静态Delay方法和async和await关键字
class Program { private static Timer _timer; static void Main(string[] args) { Status(); Console.WriteLine("C"); Console.ReadLine();//防止进程终止 //输出: // A C B A B A B... } private static async void Status() { while (true) { Console.WriteLine("A"); //在循环末尾,在不阻塞线程的前提下延迟2秒 await Task.Delay(2000);//await 运行线程返回 Console.WriteLine("B"); //2秒之后,某个线程会在await之后介入并继续循环 } } }