C# 异步编程
基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议,这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵守这种模式的方法时,手写Task通常很有用。通常TAP用起来与普通方式没什么两样,但是不支持ref和out参数。
任务和线程的区别:
1、任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。
3、Task的优势
ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
◆ ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
◆ ThreadPool不支持线程执行的先后次序;
以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,微软提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。
下面分析一理异步编程中的一些关键点
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用于计划任务稍后执行的方法。因为任务通常在线程池线程上以异步方式运行,所以,创建和启动任务的线程会在实例化任务后立即继续执行。 在某些情况下,当调用线程是主应用程序线程时,应用程序可能会在任何任务实际开始执行之前终止。 在其他情况下,应用程序的逻辑可能要求调用线程在一个或多个任务完成执行时继续执行。 可以通过调用 Wait
方法来等待一个或多个任务完成,从而同步调用线程的执行以及它启动的异步任务。若要等待单个任务完成,可以调用其 Task.Wait 方法。 Wait(Int32) 和 Wait(TimeSpan) 方法会阻止调用线程,直到任务完成或超时间隔结束。
需要注意的是,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参数意义如下:
AttachedToParent | 4 |
指定将任务附加到任务层次结构中的某个父级。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 AttachedToParent 选项以便将父任务和子任务同步。 请注意,如果使用 DenyChildAttach 选项配置父任务,则子任务中的 AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务。 |
DenyChildAttach | 8 |
指定任何尝试作为附加的子任务执行(即,使用 AttachedToParent 选项创建)的子任务都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅 附加和分离的子任务。 |
HideScheduler | 16 |
防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 Default 当前计划程序。 |
LongRunning | 2 |
指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 它会向 TaskScheduler 提示,过度订阅可能是合理的。 可以通过过度订阅创建比可用硬件线程数更多的线程。 它还将提示任务计划程序:该任务需要附加线程,以使任务不阻塞本地线程池队列中其他线程或工作项的向前推动。 |
None | 0 |
指定应使用默认行为。 |
PreferFairness | 1 |
提示 TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。 |
RunContinuationsAsynchronously | 64 |
强制异步执行添加到当前任务的延续任务。 请注意,RunContinuationsAsynchronously 成员在以 .NET Framework 4.6 开头的 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 中开发异步代码,异步方法都是首选的工具。
转载请标明本文来源:https://www.cnblogs.com/yswenli/p/11987377.html
更多内容欢迎star我的的github:https://github.com/yswenli
如果发现本文有什么问题和任何建议,也随时欢迎交流~