.NET&C#异步编程
1. 演进过程
本文档主要记录.net平台下异步编程不同时期的不同实现方案,.net平台异步编程经历了以下几次演变:
- Asynchronous Programming Model(APM):这种模式又被成为IAsyncResult模式,在.net1.0时提出,在同步方法中通过调用BeginXXXX和EndXXXX开头的方法对实现异步操作,此模式需要分配和回收IAsyncResult对象消耗资源降低效率,且不支持取消和没有提供进度报告的功能,微软不推荐使用。
- Event-based Asynchronous Pattern(EAP):它是基于事件模式的异步实现,在.net2.0时提出,这种模式具有一个或多个以Async为后缀的方法和Completed事件,它们都支持异步方法的取消、进度报告和报告结果,且其基于APM模式,此模式效率虽高,但.net中并不是所有类都支持,且业务复杂时就很难控制,微软不推荐使用。
- 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(),微软推荐使用的。
- 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. 参考信息
- 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.
- https://www.cnblogs.com/fanfan-90/p/12006157.html
- https://www.cnblogs.com/zhili/archive/2013/05/13/TAP.html
- https://www.cnblogs.com/jonins/p/9558275.html