.NET&C#异步编程

1.  演进过程

          本文档主要记录.net平台下异步编程不同时期的不同实现方案,.net平台异步编程经历了以下几次演变:

  1. Asynchronous Programming Model(APM):这种模式又被成为IAsyncResult模式,在.net1.0时提出,在同步方法中通过调用BeginXXXX和EndXXXX开头的方法对实现异步操作,此模式需要分配和回收IAsyncResult对象消耗资源降低效率,且不支持取消和没有提供进度报告的功能,微软不推荐使用。
  2. Event-based Asynchronous Pattern(EAP):它是基于事件模式的异步实现,在.net2.0时提出,这种模式具有一个或多个以Async为后缀的方法和Completed事件,它们都支持异步方法的取消、进度报告和报告结果,且其基于APM模式,此模式效率虽高,但.net中并不是所有类都支持,且业务复杂时就很难控制,微软不推荐使用。
  3. Task-based Asynchronous Pattern(TAP:task):它是基于任务模式的异步实现,在.net4.0时提出,这种模式有四种方法创建Task,1.Task.Factory.StartNew()2.(new Task(()=>{ //TODO })).Start()3.Task.Run()是.net4.5增加4.Task.FromResult(),微软推荐使用的。
  4. Task-based Asynchronous Pattern(TAP:async/await):它是基于任务模式的异步实现,在.net4.5时提出,它与第三种实现实质上相等,使用这两个关键字会使代码看起来与同步代码相当和简洁,进一步摒弃掉异步编程的复杂结构,微软极力推荐使用的异步编程模式。

2.  模式:APM和EAP

2.1.  APM

本人在WCF时期应用APM模式调用服务使用最广泛,现在除了UI交互外很少使用APM模式,以下示例仅为展示APM编码模式

 1 public void Test(){
 2     var urlStr="http://www.test.com/test/testAPM";
 3     var request=HttpWebRequest.Create(url);
 4     request.BeginGetResponse(AsyncCallbackImpl,request);//发起异步请求
 5 }
 6 public void AsyncCallbackImpl(IAsyncResult ar){
 7     var request=ar.AsyncState as HttpWebRequest;
 8     var response=request.EndGetResponse(ar);//结束异步请求
 9     using(var stream=response.GetResponseStream()){
10         var sbuilder=new StringBuilder();
11         sbuilder.AppendLine($"当前线程Id:{Thread.CurrentThread.ManagedThreadId}");
12         var reader=new StreamReader(stream);
13         sbuilder.AppendLine(reader.ReadLine());
14         Console.WriteLine(sbuilder.ToString());
15     }
16 }

 

2.2.  EAP

在大多数数据库连接驱动中使用,本人在即时通信软件中使用过,以下示例仅为展示EAP编码模式

2.2.1.  Demo:WebClient

1 public void Test(){
2     var wc=new WebClient();
3     wc.DownloadStringCompleted+=wc_DownloadStringCompleted;
4     wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
5 }
6 public void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e){
7     Console.WriteLine(e.Result);
8 }

 

2.2.2.  Demo:BackgroundWorker

 1 public void Test(){
 2     var bgworker=new BackgroundWorker();
 3     bgworker.DoWork+=bgworker_DoWork;
 4     bgworker.RunWorkerCompleted+=bgworker_RunWorkerCompleted;
 5     bgworker.RunWorkerAsync(null);//参数会被传递到DoWork事件订阅者方法中,而内部实际调用了BeginInvoke()方法
 6 }
 7 public void bgworker_DoWorker(object sender,DoWorkEventArgs e){
 8     Console.WriteLine("dowork");
 9 }
10 public void bgworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e){
11     Console.WriteLine("dowork completed");
12 }

3.  模式:TAP

3.1.  常用对象和方法

由于微软推荐使用TAP方式编码,所以本节内容是本篇文章的重点。其实TAP主要使用了以下对象和方法实现异步编程:

1)      Task<Result>:异步任务

2)      Task<Result>.ContinueWith(Action):延续任务,指定任务执行完成后延续的操作

3)      Task.Run():创建异步任务

4)      Task.WhenAll():在所有传入的任务都完成时才返回Task

5)      Task.WhenAny():在传入的任务其中一个完成就会返回Task

6)      Task.Delay():异步延时等待,示例Task.Delay(2000).Wait()

7)      Task.Yield():进入异步方法后,在await之前,如果存在耗时的同步代码,且你想让这部分代码也异步执行,那么你就可以在进入异步方法之后的第一行添加await Task.Yield()代码了,因为它会强制将当前方法转为异步执行。

3.2.  关键字:async/await

1)      使用async关键字标记的方法成为异步方法,异步方法通常包含await关键字的一个或多个实例,如果异步方法中未使用await关键字标识对象方法,那么异步方法会视为同步方法。

2)      await关键字无法等待具有void返回类型的异步方法,并且void返回方法的调用方捕获不到异步方法抛出的任何异常。

3)      异步方法无法声明in、ref或out参数,但可以调用包含此类参数的方法。

3.3.  使用示例

3.3.1.  同步方法

 1 public void Test(){
 2     Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 3     var result = SayHi("abc");
 4     Console.WriteLine(result);
 5     Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 6     Console.ReadKey();
 7 }
 8 public string SayHi(string name){
 9     Task.Delay(2000).Wait();//异步等待2s
10     Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
11     return $"Hello,{name}";
12 }

3.3.2.  异步实现

 1 public void Test(){
 2     Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 3     var result = SayHiAsync("abc").Result;
 4     Console.WriteLine(result);
 5     Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 6     Console.ReadKey();
 7 }
 8 public Task<string> SayHiAsync(string name){
 9     return Task.Run<string>(() => { return SayHi(name); });
10 }
11 public string SayHi(string name){
12     Task.Delay(2000).Wait();//异步等待2s
13     Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
14     return $"Hello,{name}";
15 }

3.3.3.  延续任务

 1 public void Test(){
 2     Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 3     var task = SayHiAsync("abc");
 4     task.ContinueWith(t=>{
 5         Console.WriteLine($"延续执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 6         var result=t.Result;
 7         Console.WriteLine(result);
 8     });
 9     Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
10         Console.ReadKey();
11 }
12 public Task<string> SayHiAsync(string name){
13     return Task.Run<string>(() => { return SayHi(name); });
14 }
15 public string SayHi(string name){
16     Task.Delay(2000).Wait();//异步等待2s
17     Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
18     return $"Hello,{name}";
19 }

3.3.4.  async/await重构

 1 public void Test(){
 2     Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 3     SayHiKeyPair("abc");
 4     Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 5     Console.ReadKey();
 6 }
 7 public async void SayHiKeyPair(string name){
 8     Console.WriteLine($"异步调用头部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
 9     var result = await SayHiAsync(name);
10     Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
11     Console.WriteLine(result);
12 }
13 public Task<string> SayHiAsync(string name){
14     return Task.Run<string>(() => { return SayHi(name); });
15 }
16 public string SayHi(string name){
17     Task.Delay(2000).Wait();//异步等待2s
18     Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
19     return $"Hello,{name}";
20 }

3.4.  运行流程

为了避免繁杂的概念,简单明了的概述为:XXXXAsync方法返回一个Task<Result>,await Task<Result>处等待异步结果,在它们中间可以执行一些与异步任务无关的逻辑。

4.  转为:TAP

4.1.  APM转化为TAP

现在将第二节中的APM实现转为TAP实现,主要借助Task.Factory.FromAsync方法

 1 public void APMtoTAP(){
 2     var urlStr="http://www.test.com/test/testAPM";
 3     var request=HttpWebRequest.Create(url);
 4         Task.Factory.FromAsync<HttpWebResponse>(request.BeginGetResponse,request.EndGetResponse,null,TaskCreationOptions.None)
 5             .ContinueWith(t=>{
 6                 var response=null;
 7                 try{
 8                     response=t.Result;
 9                     using(var stream=response.GetResponseStream()){
10                         var sbuilder=new StringBuilder();
11                         sbuilder.AppendLine($"当前线程Id:{Thread.CurrentThread.ManagedThreadId}");
12                         var reader=new StreamReader(stream);
13                         sbuilder.AppendLine(reader.ReadLine());
14                         Console.WriteLine(sbuilder.ToString());
15                     }
16                 }catch(AggregateException ex){
17                     if (ex.GetBaseException() is WebException){
18                         Console.WriteLine($"异常发生,异常信息为:{ex.GetBaseException().Message}");
19                     }else{
20                         throw;
21                     }
22                 }finally{
23                     if(response!=null){
24                         response.Close();
25                     }
26                 }
27             });
28 }

4.2.  EAP转化为TAP

 1 public void Test(){
 2     var wc=new WebClient()// WebClient类支持基于事件的异步模式(EAP)
 3     var tcs = new TaskCompletionSource<string>();//创建TaskCompletionSource和它底层的Task对象
 4 
 5     wc.DownloadStringCompleted+=(sender,e)=>{//一个string下载好之后,WebClient对象会应发DownloadStringCompleted事件
 6         if(e.Error != null){
 7             tcs.TrySetException(e.Error);//试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Faulted状态
 8         }else if(e.Cancelled){
 9             tcs.TrySetCanceled();//试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Canceled状态
10         }else{
11             tcs.TrySetResult(e.Result);//试图将基础Tasks.Task<TResult>转换为TaskStatus.RanToCompletion状态。
12         }
13 };
14     tsc.Task.ContinueWith(t=>{//为了让下面的任务在GUI线程上执行,必须标记为TaskContinuationOptions.ExecuteSynchronously
15         if(t.IsCanceled){
16             Console.WriteLine("操作已被取消");
17         }else if(t.IsFaulted){
18             Console.WriteLine("异常发生,异常信息为:" + t.Exception.GetBaseException().Message);
19         }else{
20             Console.WriteLine(String.Format("操作已完成,结果为:{0}", t.Result));
21         }
22     },TaskContinuationOptions.ExecuteSynchronously);
23         
24     wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
25 }

5.  总结

在设计异步编程时,要确定异步操作是I/O-Bound(因I/O阻塞,又称为I/O密集型),还是CPU-Bound(因CPU阻塞,又称为计算密集型),从而更好的选择方式方法。计算密集型并不是任务越多越好,如果任务数量超过CPU的核心数,那么花费在任务切换上的时间就越多,CPU的执行效率就越低。I/O密集型由于任务主要在硬盘读写和网络读写上,所以CPU就可以处理非常多的任务。

之所以有这篇文章,因为没有搜到类似本文,仅需一篇文章记录尽量全面的文章,所以就做了回搬运工,整理汇总一下。

6.  参考信息

  1. https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/#:~:text=For%20more%20information%2C%20see%20Task-based%20Asynchronous%20Pattern%20%28TAP%29.,event%20handler%20delegate%20types%2C%20and%20EventArg%20-derived%20types.
  2. https://www.cnblogs.com/fanfan-90/p/12006157.html
  3. https://www.cnblogs.com/zhili/archive/2013/05/13/TAP.html
  4. https://www.cnblogs.com/jonins/p/9558275.html

 

上一篇:MySQL压力测试工具


下一篇:Spark makeRDD方法本地Task的默认分区数