知其然知其所以然
在现在日常编码时,我们公司里的新项目,经常会用到 async 与 await 但始终不是很理解这个编程的优势和为什么要使用他,只是会用语法,会使用,今天让我们一起来了解和理解一下 Async,Await 异步同步化编程吧.
什么是异步编程?
异步编程就是编写几件事情同时发生而不会发生 “阻塞” 或 “等待” 其他事情完成的代码。这与我们的同步编程不同,在同步编程中,一切都会按照编写的顺序发生(方法与方法之间的调用前后,按照顺序执行)这就是同步代码.
C# 同步执行代码
为了方便我们更快的调试 原60秒的操作现在为6秒,30秒的操作为3秒.
PS: 定义两个方法,分别模拟获取用户信息需要时间 3秒,与调用WebService的外部接口需要时间6秒.
/// <summary>
/// 模拟调用 WebService
/// </summary>
/// <returns></returns>
public string GetRunName()
{
Task.Delay(6000).Wait(); // 等待6秒
return "Name";
}
/// <summary>
/// 模拟获取用户信息方法
/// </summary>
/// <returns></returns>
public string GetRunPhone()
{
Task.Delay(3000).Wait(); // 等待3秒
return "Phone";
}
PS: 同步编程来进行调用这两个方法.
/// <summary>
/// 同步调用
/// </summary>
/// <returns></returns>
public string GetNameAndPhone()
{
string Name = GetRunName(); // 模拟调用 WebService 返回时间为 6秒
string Phone = GetRunPhone(); // 模拟方法 返回时间为 3秒
return Name + Phone;
}
PS: 在控制台进行调试.
public class Program
{
public static void Main(string[] args)
{
AsyncDemo demo = new AsyncDemo(); // 实例化对象
string BeginDate = DateTime.Now.ToString(); // 记录方法开始时间
Console.WriteLine("开始 :" + BeginDate);
demo.GetNameAndPhone(); // 调用方法
string EndDate = DateTime.Now.ToString(); // 记录方法结束时间
Console.WriteLine("结束 :" + EndDate);
Console.WriteLine("计算时间差 :" + DateDiff(Convert.ToDateTime(BeginDate),Convert.ToDateTime(EndDate)));
Console.Read();
}
public static string DateDiff(DateTime dateTime1,DateTime dateTime2)
{
string dateDiff = null;
TimeSpan ts1 = new TimeSpan(dateTime1.Ticks);
TimeSpan ts2 = new TimeSpan(dateTime2.Ticks);
TimeSpan ts = ts1.Subtract(ts2).Duration();
dateDiff = ts.Days.ToString() + "天" + ts.Hours.ToString() + "小时" + ts.Minutes.ToString() + "分钟" + ts.Seconds.ToString() + "秒";
return dateDiff;
}
}
PS: 同步编程的执行过程为自上向下,逐步执行所以结果为 9 秒 = ( GetRunName() 6秒 + GetRunPhone() 3秒) 。 在调用此方法的过程中有 6 秒 的空闲期,被浪费了,它可以用来做其他的事情,而异步编程就可以改变这种情况.
C# 异步执行代码
/// <summary>
/// 模拟调用 WebService - 异步
/// </summary>
/// <returns></returns>
public async Task<string> GetRunNameAsync()
{
await Task.Delay(6000); // 等待6秒
return "T";
}
/// <summary>
/// 模拟获取用户信息方法
/// </summary>
/// <returns></returns>
public async Task<string> GetRunPhoneAsync()
{
await Task.Delay(3000); // 等待3秒
return "Phone";
}
PS: 异步编程来进行调用这两个方法.
/// <summary>
/// 异步调用
/// </summary>
/// <returns></returns>
public async Task<string> GetNameAndPhoneAsync()
{
// 暂时挂起执行,执行其他程序
var Name = GetRunNameAsync(); // 模拟调用 WebService 返回时间为 6 秒
var Phone = await GetRunPhoneAsync(); // 模拟获取用户信息 返回时间 3 秒
// 到此,需要知道方法的执行结果.
var Name2 = await Name;
return Name2 + Phone;
}
PS: 在控制台的 Program.Main 中进行调试,不支持Async关键字,所以我们给他包装一层MainAsync().
public class Program
{
public static void Main(string[] args)
{
AsyncDemo demo = new AsyncDemo(); // 实例化对象
string BeginDate = DateTime.Now.ToString(); // 记录方法开始时间
Console.WriteLine("开始 :" + BeginDate);
//demo.GetNameAndPhone(); // 调用同步方法
MainAsync(args).GetAwaiter().GetResult(); // 调用异步方法
string EndDate = DateTime.Now.ToString(); // 记录方法结束时间
Console.WriteLine("结束 :" + EndDate);
Console.WriteLine("计算时间差 :" + DateDiff(Convert.ToDateTime(BeginDate),Convert.ToDateTime(EndDate)));
Console.Read();
}
public static Task MainAsync(string[] args)
{
AsyncDemo demo = new AsyncDemo(); // 实例化对象
return demo.GetNameAndPhoneAsync(); // 调用异步方法
}
public static string DateDiff(DateTime dateTime1,DateTime dateTime2)
{
string dateDiff = null;
TimeSpan ts1 = new TimeSpan(dateTime1.Ticks);
TimeSpan ts2 = new TimeSpan(dateTime2.Ticks);
TimeSpan ts = ts1.Subtract(ts2).Duration();
dateDiff = ts.Days.ToString() + "天" + ts.Hours.ToString() + "小时" + ts.Minutes.ToString() + "分钟" + ts.Seconds.ToString() + "秒";
return dateDiff;
}
}
PS: 执行结果为 6秒.速度提升了3秒,如果是 60秒与30秒,那就整整提升了30秒的执行时间.
区别
1. 首先我们将方法改为了异步,加上了 async 关键字,它会告诉我们编译器这个方法可以异步执行.
2.我们使用了 await 关键字修饰了 Name 变量,它告诉我们编译器,最终我们需要 GetRunNameAsync() 方法的结果,但我们不需要阻塞这个调用,而导致程序在死等此方法的返回值.
3.我们将方法的返回类型修改为 Task<string> ,他通知调用者返回的类型为字符串,而不是立即获得,在此期间我们不只是在等待 GetRunNameAsync() 方法的结果,不能进行其他的操作.
即便是这样我们可以依然很模糊 当我们在 “等待” GetRunNameAsync() 方法返回时,我们实际在做什么?
本质上,系统希望执行 GetRunNameAsync() 方法,因为他首先被调用了,但我们在程序中并没有加上 await 等待关键字,不是需要立即知道它的结果,而程序首先调用的是 GetRunNameAsync() 方法,但它又加上了 async 关键字告诉程序此方法是异步方法,此时程序会暂时异步去处理这个方法,而不堵塞当前进程,继续执行 GetRunPhoneAsync() 方法,此方法标记了 await 关键字,所以程序会等待这个执行完成,最后我们使用了 await 关键字来等待了 GetRunNameAsync() 此方法的结果,程序已经异步执行了此方法,所以返回的结果也即时的返回给我们了,所以异步总共用时才6秒,而同步是一个方法执行完了再执行下一个,所以用了9秒.
异步返回类型
如您所见,我们使用异步编程至少对我来说还是有意义的,但是我们在 MainAsync() 用到的 Task 是什么东西呢?
含有 async 关键字的方法中有三种返回类型可以选择:
- Task:这个类表示一个异步操作,并且可以被等待;
- Task<T>:这个类表示一个有返回值的异步操作,并且可以被等待;
- 以上两个必须和 async 关键字共同使用,否则程序会出错;
- void:如果一个异步方法返回void,它不能被等待。这实际上把方法变成了“fire and forget(阅后即焚)”方法,这样的情况很少出现。进一步,返回void的异步方法的错误处理有点不同,比如shown by Stephen Cleary。没有理由使用void作为异步调用的返回类型,除非完全不关心调用是否实际完成。
PS:简而言之,几乎所有的异步方法都会使用 Task 或 Task<T> 作为它们的返回类型。Task 类表示异步操作本身,而不是action的结果。在一个 Task 中调用
await
意味着我们需要等待这个 Task 执行完成,而在 Task<T> 的情况下,需要检索任务返回的值。
潜在问题
大家在进行异步编程时,可能看不到这样戏剧性(50%)的执行速度的提升,同样我们不会对单个方法进行压力测试,只是同时执行他们。事实上,我们如果不正确的设计我们的异步方法,实际上可能会损害程序整体性能!
当我们将一个方法标记为 async 时,编译器在后台会生成一个状态机;这是额外的代码。如果我们编写好稳健的异步代码,创建这种额外结构所需时间不会对我们造成任何影响,因为运行异步的好处超过了我们构建成本机的成本。然而,如果我们 async 方法内没有 await 关键字,方法将会被同步执行,我们还需要花费额外的时间来创建我们的状态机.
还有一个潜在的问题需要我们注意:我们不能从同步方法调用异步方法,因为在所有情况下,async 和 await 应该是一起的,我们需要在所有方法上都有异步,一步异步处处异步,这就是我们为什么要单独的在 AsyncDemo 类中再单独拎出来一个 async 方法的原因。最终这导致了更多的代码,这也意味着更多的东西理论上可以中断,然而,由于我们想要完成的事情有了良好的设计和坚实的理解,拥有额外的代码也会带来性能上的大大提升,所以在我看来这是公平的交易.
细节问题:当我们在一个 async 异步方法 A 里调用了异步方法 B 我们在 A 方法中最终没有用 await 关键字来调用 B 异步方法,那么被调用的 B 异步方法如果有返回值,我们可能获取不到他的返回值 所以我们也需要用 await 来等待 B 方法的返回结果,这就是我们一步一步,处处异步.
总结
在实现异步的过程中付出一点额外的努力,对于提高我们的应用程序的性能和响应能力有很长的一段路要走,在 .NET中使用 async 和 await 关键字变得容易,我们可以简单、简洁地、快速地实现异步设计.
我在没有自己做过一遍练习和参考其他网站文档时,我对异步编程一无所知,现在最起码我不是一无所知了,希望你也一样!
参考网站: