前言

本来这篇文章上个月就该发布了,但是因为忙 QuarkDoc 一直没有时间整理,所以耽搁到今天,现在回归正轨。

C# 5.0 虽然只引入了2个新关键词:asyncawait。然而它大大简化了异步方法的编程。

在 线程池(threadPool)大致介绍了微软在不同时期使用的不同的异步模式,有3种:

1.异步模式

2.基于事件的异步模式

3.基于任务的异步模式(TAP)

而最后一种就是利用async和await关键字来实现的(TAP是现在微软极力推崇的一种异步编程方式)。

但请谨记,asyncawait关键字只是编译器功能。编译器会用Task类创建代码。如果不使用这两个关键词,用C#4.0的Task类同样可以实现相同的功能,只是没有那么方便而已。

认识asyncawait

使用asyncawait关键词编写异步代码,具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。

但是在理解上刚开始会很不习惯,而且会把一些情况想当然了,而真实情况会相去甚远(我犯过这样的错误)。所以根据几个示例一步步理解更加的靠谱些。

1.一个简单的同步方法

这是一个简单的同步方法调用示例:

     class Program
{
static void Main(string[] args)
{
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
string result = SayHi("jack");
Console.WriteLine(result);
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
static string SayHi(string name)
{
Task.Delay().Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
}

执行结果如下,方法在主线程中运行,主线程被阻塞。

2.同步方法异步化

示例将方法放到任务内执行:

     class Program
{
static void Main(string[] args)
{
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
string result = SayHiAsync("jack").Result;
Console.WriteLine(result);
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
static Task<string> SayHiAsync(string name)
{
return Task.Run<string>(() => { return SayHi(name); });
}
static string SayHi(string name)
{
Task.Delay().Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
}

执行结果如下,方法在另外一个线程中运行,因为主线程调用了Result,Result在任务没有完成时内部会使用Wait,所以主线程还是会被阻塞。

3.延续任务

示例为了避免阻塞主线程使用任务延续的方式:

     class Program
{
static void Main(string[] args)
{
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Task<string> task = SayHiAsync("jack");
task.ContinueWith(t =>//延续任务,指定任务执行完成后延续的操作
{
Console.WriteLine($"延续执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
string result = t.Result;
Console.WriteLine(result);
});
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
static Task<string> SayHiAsync(string name)
{
return Task.Run<string>(() => { return SayHi(name); });
}
static string SayHi(string name)
{
Task.Delay().Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
}

执行结果如下,方法在另外一个线程中运行,因为任务附加了延续,延续会在任务完成后处理返回值,而主线程不会被阻塞。这应该就是想要的效果了。

4.使用async和await构建异步方法调用

     class Program
{
static void Main(string[] args)
{
Console.WriteLine($"头部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
CallerWithAsync("jack");
Console.WriteLine($"尾部已执行,当前主线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
async static void CallerWithAsync(string name)
{
Console.WriteLine($"异步调用头部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
string result = await SayHiAsync(name);
Console.WriteLine($"异步调用尾部执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine(result);
}
static Task<string> SayHiAsync(string name)
{
return Task.Run<string>(() => { return SayHi(name); });
}
static string SayHi(string name)
{
Task.Delay().Wait();//异步等待2s
Console.WriteLine($"SayHi执行,当前线程Id为:{Thread.CurrentThread.ManagedThreadId}");
return $"Hello,{name}";
}
}

执行结果如下,使用await关键字来调用返回任务的异步方法SayHiAsync,而使用await需要有用async修饰符声明的方法,在SayHiAsync方法为完成前,下面的方法不会继续执行。但是主线程并没有阻塞,且任务处理完成后await后的逻辑继续执行。

本质:编译器将await关键字后的所有代码放进了延续(ContinueWith)方法的代码块中来转换await关键词。

解析asyncawait

1.异步(async)

使用async修饰符标记的方法称为异步方法,异步方法只可以具有以下返回类型:
1.Task
2.Task<TResult>
3.void
4.从C# 7.0开始,任何具有可访问的GetAwaiter方法的类型。System.Threading.Tasks.ValueTask<TResult> 类型属于此类实现(需向项目添加System.Threading.Tasks.Extensions NuGet 包)。

异步方法通常包含 await 运算符的一个或多个实例,但缺少 await 表达式也不会导致生成编译器错误。 如果异步方法未使用 await 运算符标记暂停点,那么异步方法会作为同步方法执行,即使有 async 修饰符也不例外,编译器将为此类方法发布一个警告。

2.等待(await)

await 表达式只能在由 async 修饰符标记的封闭方法体lambda 表达式或异步方法中出现。在其他位置,它会解释为标识符。

使用await运算符的任务只可用于返回 TaskTask<TResult> System.Threading.Tasks.ValueType<TResult> 对象的方法。

异步方法同步运行,直至到达其第一个 await 表达式,此时await在方法的执行中插入挂起点,会将方法挂起直到所等待的任务完成,然后继续执行await后面的代码区域。

await 表达式并不阻止正在执行它的线程。 而是使编译器将剩下的异步方法注册为等待任务的延续任务。 控制权随后会返回给异步方法的调用方。 任务完成时,它会调用其延续任务,异步方法的执行会在暂停的位置处恢复。

注意:

1.无法等待具有 void 返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法抛出的任何异常

2.异步方法无法声明 in、ref 或 out 参数,但可以调用包含此类参数的方法。 同样,异步方法无法通过引用返回值,但可以调用包含 ref 返回值的方法。

异步方法运行机理(控制流)

异步编程中最需弄清的是控制流是如何从方法移动到方法的。

下列示例及说明引自(官方文档),个人认为已经很清晰了:

     class Program
{
static void Main(string[] args)
{
var result = AccessTheWebAsync();
Console.ReadKey();
}
async static Task<int> AccessTheWebAsync()
{
HttpClient client = new HttpClient();
// GetStringAsync返回一个任务。任务Result会得到一个字符串(urlContents)。
Task<string> getStringTask = client.GetStringAsync("https://www.cnblogs.com/jonins/");
//您可以在这里完成不依赖于GetStringAsync的字符串的工作。
DoIndependentWork();
//等待的操作员暂停进入WebAsync。
//AccessTheWebAsync在getStringTask完成之前不能继续。
//同时,控制权返回到AccessTheWebAsync的调用方。
//当getStringTask完成后,控件权将继续在这里工作。 然后,await运算符从getStringTask检索字符串结果。
string urlContents = await getStringTask;
//任务完成
Console.WriteLine(urlContents.Length);
//return语句指定一个整数结果。
return urlContents.Length;
}
static void DoIndependentWork()
{
Console.WriteLine("Working..........");
}
}

多个异步方法

在一个异步方法里,可以调用一个或多个异步方法,如何编码取决于异步方法间结果是否相互依赖

1.顺序调用异步方法

使用await关键词可以调用每个异步方法,如果一个异步方法需要使用另一个异步方法的结果,await关键词就非常必要。

示例如下:

     class Program
{
static void Main(string[] args)
{
Console.WriteLine("执行前.....");
GetResultAsync();
Console.WriteLine("执行中.....");
Console.ReadKey();
}
async static void GetResultAsync()
{
var number1 = await GetResult();
var number2 = GetResult(number1);
Console.WriteLine($"结果分别为:{number1}和{number2.Result}");
}
static Task<int> GetResult(int number)
{
return Task.Run<int>(() => { Task.Delay().Wait(); return number + ; });
}
}

2.使用组合器

如果异步方法间相互不依赖,则每个异步方法都不使用await,而是把每个异步方法的结果赋值给Task变量,就会运行得更快。

示例如下:

     class Program
{
static void Main(string[] args)
{
Console.WriteLine("执行前.....");
GetResultAsync();
Console.WriteLine("执行中.....");
Console.ReadKey();
}
async static void GetResultAsync()
{
Task<int> task1 = GetResult();
Task<int> task2 = GetResult();
await Task.WhenAll(task1, task2);
Console.WriteLine($"结果分别为:{task1.Result}和{task2.Result}");
}
static Task<int> GetResult(int number)
{
return Task.Run<int>(() => { Task.Delay().Wait(); return number + ; });
}
}

Task类定于2个组合器分别为:WhenAllWhenAny

WhenAll是在所有传入的任务都完成时才返回Task

WhenAny是在传入的任务其中一个完成就会返回Task

异步方法的异常处理

1.异常处理

以下示例一种是普通的错误的捕获方式,另一种是异步方法异常捕获方式:

     class Program
{
static void Main(string[] args)
{ DontHandle();
HandleError();
Console.ReadKey();
}
//错误处理
static void DontHandle()
{
try
{
var task = ThrowAfter(, "DontHandle Error");
}
catch (Exception ex)
{ Console.WriteLine(ex.Message);
}
}
//异步方法错误处理
static async void HandleError()
{
try
{
await ThrowAfter(, "HandleError Error");
}
catch (Exception ex)
{ Console.WriteLine(ex.Message);
}
}
//在延迟后抛出异常
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}
}

执行结果如下:

调用异步方法,如果只是简单的放在try/catch块中,将会捕获不到异常这是因为DontHandle方法在ThrowAfter抛出异常之前已经执行完毕(返回void的异步方法不会等待。这是因为从async void方法抛出的异常无法捕获。因此异步方法最好返回一个Task类型)。

异步方法的一个较好异常处理方式是使用await关键字,将其放在try/catch中

2.多个异步方法异常处理

如果调用了多个异步方法,在第一个异步方法抛出异常,后续的方法将不会被调用,catch块内只会处理出现的第一个异常。

所以正确的做法是使用Task.WhenAll,不管任务是否抛出异常都会等到所有任务完成。Task.WhenAll结束后,异常被catch语句捕获到。如果只是捕获Exception,我们只能看到WhenAll方法的第一个发生异常的任务信息,不会抛出后续的异常任务

如果要捕获所有任务的异常信息,就是对任务声明变量,在catch块内可以访问,再使用IsFaulted属性检查任务的状态,以确认它们是否出现错误,然后再进行处理。示例如下:

     class Program
{
static void Main(string[] args)
{
HandleError();
Console.ReadKey();
}
//正确的处理方式
static async void HandleError()
{
Task t1 = null;
Task t2 = null;
try
{
t1 = ThrowAfter(, "HandleError-One-Error");
t2 = ThrowAfter(, "HandleError-Two-Error");
await Task.WhenAll(t1, t2);
}
catch (Exception)
{
if (t1.IsFaulted)
Console.WriteLine(t1.Exception.InnerException.Message);
if (t2.IsFaulted)
Console.WriteLine(t2.Exception.InnerException.Message);
}
}
//在延迟后抛出异常
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}
}

3.使用AggregateException捕获异步方法异常

任务(task)中介绍过AggregateException,它包含了等待中所有异常的列表,可轻松遍历处理所有异常信息。示例如下:

     class Program
{
static void Main(string[] args)
{
HandleError();
Console.ReadKey();
}
//正确的处理方式
static async void HandleError()
{
Task taskResult = null;
try
{
Task t1 = ThrowAfter(, "HandleError-One-Error");
Task t2 = ThrowAfter(, "HandleError-Two-Error");
await (taskResult = Task.WhenAll(t1, t2));
}
catch (Exception)
{
foreach (var ex in taskResult.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
} }
}
//在延迟后抛出异常
static async Task ThrowAfter(int ms, string message)
{
await Task.Delay(ms);
throw new Exception(message);
}
}

重要的补充与建议

1.提高响应能力

.NET有很多异步API我们都可以通过async/await构建调用提高响应能力,例如:

     class Program
{
static void Main(string[] args)
{
Demo();
Console.ReadKey();
}
static async void Demo()
{
HttpClient httpClient = new HttpClient();
var getTaskResult = await httpClient.GetStringAsync("https://www.cnblogs.com/jonins/");
Console.WriteLine(getTaskResult);
}
}

这些API都有相同原则即以Async结尾。

2.重要建议

1.async方法需在其主体中具有await 关键字,否则它们将永不暂停。同时C# 编译器将生成一个警告,此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。

2.应将“Async”作为后缀添加到所编写的每个异步方法名称中。这是 .NET 中的惯例,以便更轻松区分同步和异步方法。

3.async void 应仅用于事件处理程序。因为事件不具有返回类型(因此无法返回 Task 和 Task<T>)。 其他任何对 async void 的使用都不遵循 TAP 模型,且可能存在一定使用难度。

例如:async void 方法中引发的异常无法在该方法外部被捕获或十分难以测试 async void 方法。

3.以非阻止方式处理等待任务

异步编程准则

异步编程的准则确定所需执行的操作是I/O-Bound还是 CPU-Bound。因为这会极大影响代码性能,并可能导致某些构造的误用。

考虑两个问题:

1.你的代码是否会“等待”某些内容,例如数据库中的数据或web资源等?如果答案为“是”,则你的工作是 I/O-Bound

2.你的代码是否要执行开销巨大的计算?如果答案为“是”,则你的工作是 CPU-Bound

如果你的工作为 I/O-Bound,请使用 asyncawait(而不使用 Task.Run)。 不应使用任务并行库。 
如果你的工作为 CPU-Bound,并且你重视响应能力,请使用 asyncawait,并在另一个线程上使用 Task.Run 生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。

结语

如果想要了解状态机请戳:这里 。

参考资料

C#高级编程(第10版) C# 6 & .NET Core 1.0   Christian Nagel

果壳中的C# C#5.0权威指南  Joseph Albahari

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/await

异步编程(async&await)的更多相关文章

  1. 异步编程Async/await关键字

    异步编程Async \await 关键字在各编程语言中的发展(出现)纪实. 时间 语言版本 2012.08.15 C#5.0(VS2012) 2015.09.13 Python 3.5 2016.03 ...

  2. 抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

    长话短说,本文带大家抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext 引言 C#异步编程语法糖async/await,使开发者很容易就能编写异步代码. ...

  3. 温故知新,CSharp遇见异步编程(Async/Await),聊聊异步编程最佳做法

    什么是异步编程(Async/Await) Async/Await本质上是通过编译器实现的语法糖,它让我们能够轻松的写出简洁.易懂.易维护的异步代码. Async/Await是C# 5引入的关键字,用以 ...

  4. javascript异步编程 Async/await

    Async/await Async/await 在学习他之前应当补充一定的 promise 知识 它是一种与 promise 相配合的特殊语法,目前被认为是异步编程的终级解决方案 值得我们每一个人学习 ...

  5. [C#] 谈谈异步编程async await

    为什么需要异步,异步对可能起阻止作用的活动(例如,应用程序访问 Web 时)至关重要. 对 Web 资源的访问有时很慢或会延迟. 如果此类活动在同步过程中受阻,则整个应用程序必须等待. 在异步过程中, ...

  6. .net 异步编程async & await关键字的思考

    C# 5.0引入了两个关键字 async和await,这两个关键字在很大程度上帮助我们简化了异步编程的实现代码,而且TPL中的task与async和await有很大的关系 思考了一下异步编程中的asy ...

  7. 异步编程async/await

    什么是异步? 在异步程序中,程序代码不需要按照编写时的顺序严格执行,有时需要一在一个新的线程中运行一部分代码,有时无需创建新的 线程,但是为了更好的利用单个线程的能力,需要改变代码的执行顺序. 进程 ...

  8. c#异步编程async await

    可以代替协程了 但是需要.net4 版本 unity2017以上版本可以用了 再也可以不用蛋疼的没有返回值的协程了 //异步编程,和Task一起用 async void TestAsync(){ // ...

  9. .NetCore 异步编程 - async/await

    前言: 这段时间开始用.netcore做公司项目,发现前辈搭的框架通篇运用了异步编程方式,也就是async/await方式,作为一个刚接触的小白,自然不太明白其中原理,最重要的是,这个玩意如果不明白基 ...

  10. .net 温故知新:【5】异步编程 async await

    1.异步编程 异步编程是一项关键技术,可以直接处理多个核心上的阻塞 I/O 和并发操作. 通过 C#.Visual Basic 和 F# 中易于使用的语言级异步编程模型,.NET 可为应用和服务提供使 ...

随机推荐

  1. python第五十一天----线程,Event,队列

    进程与线程的区别: 线程==指令集,进程==资源集  (线程集) 1.同一个进程中的线程共享内存空间,进程与进程之间是独立的 2.同一个进程中的线程是可以直接通讯交流的,进程与间通讯必需通过一个中间的 ...

  2. Windows10 内存泄漏

    之前遇到win10开机idle一段时间后, 内存噌噌的往上彪, 16G内存基本什么东西没开就90%多.查了网上的一些解决方案: 方法1. 关闭Ndu服务 sc config Ndu start=dis ...

  3. JUnit单元测试入门

    什么是单元测试 写了个类,要给别人用,会不会有bug?怎么办?测试一下. 用main方法测试好不好?不好! 不能一起运行! 大多数情况下需要人为的观察输出确定是否正确 为什么要进行单元测试 重用测试, ...

  4. 戴尔dell服务器硬件故障SMTP邮件报警idrac配置

    上次公司有台戴尔的服务器硬盘故障了却没有及时发现,后面就研究了一下看到戴尔的idrac有硬件SMTP报警功能 然后自己折腾了一下,一开始配置不成功,后面问了一下戴尔官方的最好自己再摸索了一下解决了,做 ...

  5. QSetting的值不能保存。

    最近在使用QSetting的时候,setting的值死活保存不下来,后来添加了如何设置后,settting的可以获取到. QCoreApplication::setOrganizationName(& ...

  6. 4.2Python数据处理篇之Matplotlib系列(二)---plt.scatter()散点图

    目录 目录 前言 (一)散点图的基础知识 (二)相关性的举例 ==1.正相关== ==1.负相关== ==1.不相关== (三)实战项目以一股票的分析 目录 前言 散点图是用于观测数据的相关性的,有正 ...

  7. 记录线上一次线程hang住问题

    线上发现执行某特定任务在某个特定时间点后不再work.该任务由线程池中线程执行定时周期性调度,根据日志查看无任何异常.从代码研判应该无关定时任务框架,因为对提交的定时任务做了wrap,会将异常都cat ...

  8. B - Modular Inverse

    The modular modular multiplicative inverse of an integer a modulo m is an integer x such that a-1≡x ...

  9. springboot 传值到页面

    每天学习一点点 编程PDF电子书.视频教程免费下载:http://www.shitanlife.com/code   <!DOCTYPE html> 2 <html> 3 &l ...

  10. 最近公共祖先(LCA)模板

    第一行包含三个正整数N.M.S,分别表示树的结点个数.询问的个数和树根结点的序号. 接下来N-1行每行包含两个正整数x.y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树). 接下来M行 ...