基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议,这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵守这种模式的方法时,手写Task通常很有用。通常TAP用起来与普通方式没什么两样,但是不支持ref和out参数。 任务和线程的区别: 2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。 3、Task的优势 ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如: 下面分析一理异步编程中的一些关键点 1.await我们都知道await关键字是.Net FrameWork4.5引入的特性。await使得我们使用异步更加时特别便捷,并且还不会导致线程堵塞。我们在使用时也就莫名其妙的使用。往往不知道为什么不会导致线程堵塞。在这里,简单的谈论下await的一点原理。 在c#并行编程这本书中是这么介绍await的:async方法在开始时以同步方式执行,在async方法内部,await关键字对它参数执行一个异步等待,它首先检查操作是否已经完成,如果完成,就继续运行(同步方式),否则,会暂停async方法,并返回.留下一个未完成的task,一段时间后,操作完成,async方法就恢复执行. 看到这句话应该就差不多能想到await为什么不会导致线程堵塞了,当碰到await时如果没有执行成功就先暂停这个方法的执行,执行方法外以下代码,等await操作完成后再执行这个方法await之后的代码;直白点的意思就是async/await就是一个让编译器把现有的代码直接生成一个带回调逻辑代码的语法糖。 先添加一个方法,如下: ![]() 1 async Task DemoAsync() 2 { 3 await Task.Run(() => { }); 4 Thread.Sleep(3000); 5 } 接着添加按钮事件方法: ![]() 1 private void button1_Click(object sender, EventArgs e) 2 { 3 DemoAsync(); 4 MessageBox.Show("同步代码"); 5 } 接下来修改代码后再次运行后,会发现在点击button按钮时窗体不能被移动了,然后等待了3秒钟才弹出"同步代码"这句话。 ![]() 1 async Task DemoAsync() 2 { 3 await Task.Run(() => { Thread.Sleep(3000); }); 4 Thread.Sleep(3000); 5 } 最后运行的时候就会神奇的发现,此时会先弹出"同步代码"这局话,然后等待3秒后窗体就不能被移动。 2.Task Task 类表示通常以异步方式执行的单个操作, Task 对象是基于任务的异步模式的中心组件之一。 由于 Task 对象执行的工作通常在线程池线程上异步执行,而不是在主应用程序线程上同步执行,因此可以使用 Status 属性,还可以使用 IsCanceled、IsCompleted和 IsFaulted 属性,用于确定任务的状态。 通常,lambda 表达式用于指定任务要执行的工作。可以通过多种方式创建 Task 实例。 最常见的方法(从 .NET Framework 4.5开始提供)是调用静态 Run 方法。 Run 方法提供一种简单的方法来使用默认值启动任务,而无需其他参数。Task 类还提供了初始化任务的构造函数,但不计划执行该任务。 出于性能原因,Task.Run 或 TaskFactory.StartNew 方法是用于创建和计划计算任务的首选机制,但对于必须分隔创建和计划的情况,可以使用构造函数,然后调用 Task.Start用于计划任务稍后执行的方法。因为任务通常在线程池线程上以异步方式运行,所以,创建和启动任务的线程会在实例化任务后立即继续执行。 在某些情况下,当调用线程是主应用程序线程时,应用程序可能会在任何任务实际开始执行之前终止。 在其他情况下,应用程序的逻辑可能要求调用线程在一个或多个任务完成执行时继续执行。 可以通过调用 需要注意的是,Task中的方法一般是不会将异常抛出的,哪怕是Task<T>这种,这需要开发人员自行处理。不过如果获取Task<T>.Result则会将任务内的异常抛出来; ![]() 1 public static async Task<string> Hello(int a=0) 2 { 3 return await Task.Run(async () => 4 { 5 await Task.Delay(1000).ConfigureAwait(true); 6 7 return $"Hello {10/a}"; 8 9 }).ConfigureAwait(true); 10 } 11 //不会报异常 12 Hello(0); 13 //会报异常 14 var a1= Hello(0).Result; Task.Factory提供了更多参数的Task创建方式以支持自定义的Task,比如:Task,Factory.StartNew(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler)就支持指定取消通知参数、任务创建模式,指定任务调度器;其中TaskCreationOptions参数意义如下:
3.Task.ConfigureAwait在Task里中有ConfigureAwait这么一个方法,这个方法是干什么的呢,我们先看下方法注释是怎么解释这个方法的:" 尝试将延续任务封送回原始上下文,则为 true;否则为 false。" 光看这段代码并看不出什么,然后我们再看这么一段话:"一个async方法是由多个同步执行的程序块组成.每个同步程序块之间由await语句分隔.用await语句等待一个任务完成.当该方法在await处暂停时,就可以捕捉上下文(context).如果当前SynchronizationContext不为空,这个上下文就是当前SynchronizationContext.如果为空,则这个上下文为当前TaskScheduler.该方法会在这个上下文中继续运行.一般来说,运行UI线程时采用UI上下文,处理ASP.NET请求时采用ASP.NET请求上下文,其它很多情况则采用线程池上下文。" 这句话已经基本讲明了其实后续代码会下上文中执行。这个上下文一般时UI上下文(运行在UI上)或请求上下文(ASP.NET) 这两个可以说时原始上下文,而其它情况采用线程池上下文,也就是开辟一个新线程。这么说也就是ConfigureAwait方法是将后续代码是送到原始上下文还是线程池上下文中。 下面稍微修改下刚才的方法: ![]() 1 async Task DemoAsync() 2 { 3 //将后续代码交给线程池执行 4 await Task.Run(() => { Thread.Sleep(3000); }).ConfigureAwait(false); 5 Thread.Sleep(3000); 6 } ConfigureAwait(false)将后续代码交给线程池来执行,也就是上面的Thread.Sleep并不会阻塞窗体。 4.Task.Delay在异步编程中,一般不建议使用Thread.Sleep,而是使用粒度更小的Task.Delay;Thread.Sleep、Thread.Yeild等会让当前工作线程阻塞,而Task.Delay可以让当前线程空出来去完成其他的Task。 ![]() 1 CancellationTokenSource source = new CancellationTokenSource(); 2 3 var t = Task.Run(async delegate 4 { 5 await Task.Delay(TimeSpan.FromSeconds(1.5), source.Token); 6 return 42; 7 }); 8 source.Cancel(); 9 try { 10 t.Wait(); 11 } 12 catch (AggregateException ae) { 13 foreach (var e in ae.InnerExceptions) 14 Console.WriteLine("{0}: {1}", e.GetType().Name, e.Message); 15 } 5.SemaphoreSlim表示对可同时访问资源或资源池的线程数加以限制的 Semaphore 的轻量替代, SemaphoreSlim类是用于在单个应用内进行同步的建议信号量。在异步编程过程中,建议使用SemaphoreSlim.Wait()、SemaphoreSlim.Release()来替换Lock,因为lock只能由当前线程来解锁,而SemaphoreSlim可以由任意线程来解锁。 6.CancellationTokenSource启动一个Task去做一些事,如果希望它在某个阶段去被动的停止,可以使用这个CancellationTokenSource对象,把它注入到Task里,使用当外界触发Cancel()方法或设置了超时时,这个Task就会被取消。通常和CancellationToken.IsCancellationRequested一起配合Task来使用。 ![]() 1 public static async void Loop(int timeOut = 5 * 1000) 2 { 3 using (CancellationTokenSource source = new CancellationTokenSource(timeOut)) 4 { 5 CancellationToken token = source.Token; 6 7 //匿名异步方法 8 var task1 = Task.Run(async () => 9 { 10 while (!token.IsCancellationRequested) 11 { 12 Console.WriteLine($"{DateTime.Now.ToLongTimeString()}\t TaskTest Loop"); 13 await Task.Delay(1000).ConfigureAwait(false); 14 } 15 Console.WriteLine($"{DateTime.Now.ToLongTimeString()}\t TaskTest Loop Timeout Stoped"); 16 }, token); 17 18 await Task.WhenAll(task1).ConfigureAwait(false); 19 } 20 } 也可以CancellationTokenSource.CreateLinkedTokenSource来关联多个CancellationTokenSource ![]() 1 using (CancellationTokenSource cts = new CancellationTokenSource()) 2 { 3 var cancellationToken = cts.Token; 4 5 using (CancellationTokenSource timeoutSource = new CancellationTokenSource(timeOut * 1000)) 6 7 using (CancellationTokenSource linkSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, cancellationToken)) 8 { 9 var task = Task.Run(() => 10 { 11 ... 12 }, linkSource.Token); 13 14 //引发异步异常 15 if (a == 10) 16 { 17 cts.Cancel(); 18 } 19 20 return await task2.ConfigureAwait(true); 21 } 22 } 7.TaskCompletionSource表示未绑定到委托的 Task<TResult> 的制造者方,并通过 Task 属性提供对使用者方的访问。从这个官方解释上看不出这个到底有什么作用,其实这是一种受用者控制创建Task的方式。你可以使Task在任何你想要的时候完成,你也可以在任何地方给它一个异常让它失败。这个可以实现事件通知类似的功能;具体就是说TaskCompletionSource如果不进行SetResult或SetException的时候,TaskCompletionSource所委托的的Task是不会有Result,这个Task会一直等待TaskCompletionSource来赋值;这样就极大的简化了异步事件或异步通知的实现。 ![]() 1 public static async Task<int> SetValue(int a = 1) 2 { 3 var sw = Stopwatch.StartNew(); 4 5 TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(); 6 7 await Task.Run(async () => 8 { 9 try 10 { 11 await Task.Delay(10 * 1000).ConfigureAwait(true); 12 //此处若不赋值,测tcs.Task.Result会一直等待 13 tcs.SetResult(10 / a); 14 } 15 catch (Exception ex) 16 { 17 tcs.SetException(ex); 18 } 19 }).ConfigureAwait(true); 20 21 22 return await Task.Run(() => 23 { 24 int result = 0; 25 try 26 { 27 result = tcs.Task.Result; 28 29 Console.WriteLine("tcs取值成功!"); 30 } 31 catch (AggregateException e) 32 { 33 Console.WriteLine("tcs异常"); 34 35 for (int j = 0; j < e.InnerExceptions.Count; j++) 36 { 37 Console.WriteLine($"\n-------------------------------------------------\n{e.InnerExceptions[j].ToString()}\n-------------------------------------------------\n"); 38 } 39 } 40 sw.Stop(); 41 Console.WriteLine($"(ElapsedTime={sw.ElapsedMilliseconds})"); 42 return result; 43 }).ConfigureAwait(true); 44 }
8.异步性能异步方法是一个功能强大的高效工具,使您能够更轻松编写可伸缩和响应更快的库和应用程序。 请牢记一点,异步不是对单个操作的性能优化。 采用同步操作并使其异步化必然会降低该操作的性能,因为它仍然需要完成同步操作的所有工作,只不过现在会有额外的限制和注意事项。 关注异步的一个原因是其总体性能:如果采用异步方法编写所有内容,整个系统的执行效果如何。这样仅消耗执行需要的有价值的资源,重叠 I/O 并实现更好的系统利用率。从现在开始,无论何时准备在 .NET Framework 中开发异步代码,异步方法都是首选的工具。
|
|