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编码模式
public void Test(){
var urlStr="http://www.test.com/test/testAPM";
var request=HttpWebRequest.Create(url);
request.BeginGetResponse(AsyncCallbackImpl,request);//发起异步请求
}
public void AsyncCallbackImpl(IAsyncResult ar){
var request=ar.AsyncState as HttpWebRequest;
var response=request.EndGetResponse(ar);//结束异步请求
using(var stream=response.GetResponseStream()){
var sbuilder=new StringBuilder();
sbuilder.AppendLine($"当前线程Id:{Thread.CurrentThread.ManagedThreadId}");
var reader=new StreamReader(stream);
sbuilder.AppendLine(reader.ReadLine());
Console.WriteLine(sbuilder.ToString());
}
}
2.2. EAP
在大多数数据库连接驱动中使用,本人在即时通信软件中使用过,以下示例仅为展示EAP编码模式
2.2.1. Demo:WebClient
public void Test(){
var wc=new WebClient();
wc.DownloadStringCompleted+=wc_DownloadStringCompleted;
wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
}
public void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e){
Console.WriteLine(e.Result);
}
2.2.2. Demo:BackgroundWorker
public void Test(){
var bgworker=new BackgroundWorker();
bgworker.DoWork+=bgworker_DoWork;
bgworker.RunWorkerCompleted+=bgworker_RunWorkerCompleted;
bgworker.RunWorkerAsync(null);//参数会被传递到DoWork事件订阅者方法中,而内部实际调用了BeginInvoke()方法
}
public void bgworker_DoWorker(object sender,DoWorkEventArgs e){
Console.WriteLine("dowork");
}
public void bgworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e){
Console.WriteLine("dowork completed");
}
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. 同步方法
public void Test(){
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
var result = SayHi("abc");
Console.WriteLine(result);
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public string SayHi(string name){
Task.Delay(2000).Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
3.3.2. 异步实现
public void Test(){
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
var result = SayHiAsync("abc").Result;
Console.WriteLine(result);
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public Task<string> SayHiAsync(string name){
return Task.Run<string>(() => { return SayHi(name); });
}
public string SayHi(string name){
Task.Delay(2000).Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
3.3.3. 延续任务
public void Test(){
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
var task = SayHiAsync("abc");
task.ContinueWith(t=>{
Console.WriteLine($"延续执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
var result=t.Result;
Console.WriteLine(result);
});
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public Task<string> SayHiAsync(string name){
return Task.Run<string>(() => { return SayHi(name); });
}
public string SayHi(string name){
Task.Delay(2000).Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
3.3.4. async/await重构
public void Test(){
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
SayHiKeyPair("abc");
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public async void SayHiKeyPair(string name){
Console.WriteLine($"异步调用头部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
var result = await SayHiAsync(name);
Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine(result);
}
public Task<string> SayHiAsync(string name){
return Task.Run<string>(() => { return SayHi(name); });
}
public string SayHi(string name){
Task.Delay(2000).Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
3.4. 运行流程
为了避免繁杂的概念,简单明了的概述为:XXXXAsync方法返回一个Task<Result>,await Task<Result>处等待异步结果,在它们中间可以执行一些与异步任务无关的逻辑。
4. 转为:TAP
4.1. APM转化为TAP
现在将第二节中的APM实现转为TAP实现,主要借助Task.Factory.FromAsync方法
public void APMtoTAP(){
var urlStr="http://www.test.com/test/testAPM";
var request=HttpWebRequest.Create(url);
Task.Factory.FromAsync<HttpWebResponse>(request.BeginGetResponse,request.EndGetResponse,null,TaskCreationOptions.None)
.ContinueWith(t=>{
var response=null;
try{
response=t.Result;
using(var stream=response.GetResponseStream()){
var sbuilder=new StringBuilder();
sbuilder.AppendLine($"当前线程Id:{Thread.CurrentThread.ManagedThreadId}");
var reader=new StreamReader(stream);
sbuilder.AppendLine(reader.ReadLine());
Console.WriteLine(sbuilder.ToString());
}
}catch(AggregateException ex){
if (ex.GetBaseException() is WebException){
Console.WriteLine($"异常发生,异常信息为:{ex.GetBaseException().Message}");
}else{
throw;
}
}finally{
if(response!=null){
response.Close();
}
}
});
}
4.2. EAP转化为TAP
public void Test(){
var wc=new WebClient()// WebClient类支持基于事件的异步模式(EAP)
var tcs = new TaskCompletionSource<string>();//创建TaskCompletionSource和它底层的Task对象
wc.DownloadStringCompleted+=(sender,e)=>{//一个string下载好之后,WebClient对象会应发DownloadStringCompleted事件
if(e.Error != null){
tcs.TrySetException(e.Error);//试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Faulted状态
}else if(e.Cancelled){
tcs.TrySetCanceled();//试图将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Canceled状态
}else{
tcs.TrySetResult(e.Result);//试图将基础Tasks.Task<TResult>转换为TaskStatus.RanToCompletion状态。
}
};
tsc.Task.ContinueWith(t=>{//为了让下面的任务在GUI线程上执行,必须标记为TaskContinuationOptions.ExecuteSynchronously
if(t.IsCanceled){
Console.WriteLine("操作已被取消");
}else if(t.IsFaulted){
Console.WriteLine("异常发生,异常信息为:" + t.Exception.GetBaseException().Message);
}else{
Console.WriteLine(String.Format("操作已完成,结果为:{0}", t.Result));
}
},TaskContinuationOptions.ExecuteSynchronously);
wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
}
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