.net 温故知新:【5】异步编程 async await
2021/8/26 9:06:12
本文主要是介绍.net 温故知新:【5】异步编程 async await,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
1、异步编程
异步编程是一项关键技术,可以直接处理多个核心上的阻塞 I/O 和并发操作。 通过 C#、Visual Basic 和 F# 中易于使用的语言级异步编程模型,.NET 可为应用和服务提供使其变得可响应且富有弹性。
上面是关于异步编程的解释,我们日常编程过程或多或少的会使用到异步编程,为什么要试用异步编程?因为用程序处理过程中使用文件和网络 I/O,比如处理文件的读取写入磁盘,网络请求接口API,默认情况下 I/O API 一般会阻塞。
这样的结果是导致我们的用户界面卡住体验差,有些服务器的硬件利用率低,服务处理能力请求响应慢等问题。基于任务的异步 API 和语言级异步编程模型改变了这种模型,只需了解几个新概念就可默认进行异步执行。
现在普遍使用的异步编程模式是TAP模式,也就是C# 提供的 async 和 await 关键词,实际上我们还有另外两种异步模式:基于事件的异步模式 (EAP),以及异步编程模型 (APM) 。
APM 是基于 IAsyncResult 接口提供的异步编程,例如像FileStream类的BeginRead,EndRead就是APM实现方式,提供一对开始结束方法用来启动和接受异步结果。使用委托的BeginInvoke和EndInvoke的方式来实现异步编程。
EAP 是在 .NET Framework 2.0 中引入的,比较多的体现在WinForm编程中,WinForm编程中很多控件处理事件都是基于事件模型,经常用到跨线程更新界面的时候就会使用到BeginInvoke和Invoke。事件模式算是对APM的一种补充,定义了一系列事件包括完成、进度、取消的事件让我们在异步调用的时候能注册响应的事件进行操作。
class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now + " start"); IAsyncResult result = BeginAPM(); //EndAPM(result); Console.WriteLine(DateTime.Now + " end"); Console.ReadKey(); } delegate void DelegateAPM(); static DelegateAPM delegateAPM = new DelegateAPM(DelegateAPMFun); public static IAsyncResult BeginAPM() { return delegateAPM.BeginInvoke(null, null); } public static void EndAPM(IAsyncResult result) { delegateAPM.EndInvoke(result); } public static void DelegateAPMFun() { Console.WriteLine("DelegateAPMFun...start"); Thread.Sleep(5000); Console.WriteLine("DelegateAPMFun...end"); } }
如上代码我使用委托实现异步调用,BeginAPM 方法使用 BeginInvoke 开始异步调用,然后 DelegateAPMFun 异步方法里面停5秒。看下下面的打印结果,是 main 方法里面的打印在前,异步方法里面的打印在后,说明该操作是异步的。
其中一行代码EndAPM(result)
被注释了,调用了委托 EndInvoke 方法,该方法会阻塞程序直到异步调用完成,所以我们可以放到适当的位置用来获取执行结果,这类似于TAP模式的await 关键字,放开改行代码执行下。
以上两种方式已不推荐使用,编写理解起来比较晦涩,感兴趣的可以自行了解下,而且这种方式在.net 5里面已经不支持委托的异步调用了,所以如果要运行需要在.net framework框架下。
TAP 是在 .NET Framework 4 中引入的,是目前推荐的异步设计模式,也是我们本文讨论的重点方向,但是TAP并不一定是线程,他是一种任务,理解为工作的异步抽象,而非在线程之上的抽象。
2、async await
使用 async await 关键字可以很轻松的实现异步编程,我们子需要将方法加上 async 关键字,方法内的异步操作使用 await 等待异步操作完成后再执行后续操作。
class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now + " start"); AsyncAwaitTest(); Console.WriteLine(DateTime.Now + " end"); Console.ReadKey(); } public static async void AsyncAwaitTest() { Console.WriteLine("test start"); await Task.Delay(5000); Console.WriteLine("test end"); } }
AsyncAwaitTest 方法使用 async 关键字,使用await关键字等待5秒后打印"test end"。在 Main 方法里面调用 AsyncAwaitTest 方法。
使用 await 在任务完成前将控制让步于其调用方,可让应用程序和服务执行有用工作。 任务完成后代码无需依靠回调或事件便可继续执行。 语言和任务 API 集成会为你完成此操作。
使用await 的方法必须使用 async 关键字,如果我们 Main 方法里面想等待 AsyncAwaitTest 则 Main 方法需要加上 async 并返回 Task。
3、async await 原理
将上面 Main 方法不使用 await 调用的方式编译后使用ILSpy反编译dll,使用C# 4.0才能看到编译器为我们做了什么。因为4.0不支持 async await 所以会反编译到具体代码,4.0 以后的反编译后会直接显示 async await 语法。
通过反编译后可以看到在异步方法里面重新生成了一个泛型类 d__1 实现接口IAsyncStateMachine,然后调用Start方法,Start中进行了一些线程处理后调用 stateMachine.MoveNext()
即调用d__1实例化对象的MoveNext方法。
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { if (stateMachine == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); } Thread currentThread = Thread.CurrentThread; Thread thread = currentThread; ExecutionContext executionContext = currentThread._executionContext; ExecutionContext executionContext2 = executionContext; SynchronizationContext synchronizationContext = currentThread._synchronizationContext; try { stateMachine.MoveNext(); } finally { SynchronizationContext synchronizationContext2 = synchronizationContext; Thread thread2 = thread; if (synchronizationContext2 != thread2._synchronizationContext) { thread2._synchronizationContext = synchronizationContext2; } ExecutionContext executionContext3 = executionContext2; ExecutionContext executionContext4 = thread2._executionContext; if (executionContext3 != executionContext4) { ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4); } } }
我们再看编译器为生成的类 <AsyncAwaitTest>d__1
:
MoveNext方法将 AsyncAwaitTest 逻辑代码包含进去了,我们的源代码因为只有一个 await 操作,如果有多个 await 操作,那么MoveNext里面应该还会有多个分段逻辑,将不同段的MoveNext放入不同的状态分段块。
在该类中也有一个if判断,按照 1__state 状态参数,最开始调用的时候是-1,执行进来 num != 0
则执行我们的业务代码if里面的,这个时候会顺序执行业务代码,直到碰到 await 则执行如下代码
awaiter = Task.Delay(5000).GetAwaiter(); if (!awaiter.IsCompleted) { num = (<> 1__state = 0); <> u__1 = awaiter; < AsyncAwaitTest > d__1 stateMachine = this; <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; }
在该过程中 <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
将 await 句和状态机进行传递调用 AwaitUnsafeOnCompleted
方法,该方法一直跟下去会找到线程池的操作。
// System.Threading.ThreadPool internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal) { s_workQueue.Enqueue(callBack, !preferLocal); }
程序将封装的任务放入线程池进行调用,这个时候异步方法就切换到了另一个线程,或者在原线程上执行(如果异步方法执行时间比较短可能就不会进行线程切换,这个主要看调度程序)。
执行完成 await 后状态 1__state 已经更改了为 0,程序会再次调用 MoveNext 进入 else 之后没有return和其它逻辑,则继续执行到结束。
可以看到这是一个状态控制的执行逻辑,是一种“状态机模式”的设计模式,对于 Main 方法调用 AsyncAwaitTest 逻辑此刻进入if,碰到await则进入线程调度执行,如果异步方法切换到其它线程调用,则方法 Main 继续执行,当状态机执行切换到另外一个状态后再次 MoveNext 直到执行完异步方法。
4、async 与 线程
有了上面的基础我们知道 async 与 await 通常是成对配合使用的,当我们的方法标记为异步的时候,里面的耗时操作就需要 await 进行标记等待完成后执行后续逻辑,调用该异步方法的调用者可以决定是否等待,如果不用 await 则调用者异步执行或者就在原线程上执行异步方法。
如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行,可选择性通过 Task.Run API 显式请求任务在独立线程上运行。
可以将 AsyncAwaitTest 方法改为显示线程运行:
public static async Task AsyncAwaitTest() { Console.WriteLine("test start"); await Task.Run(() => { Thread.Sleep(5000); }); Console.WriteLine("test end"); }
5、取消任务 CancellationToken
如果不想等待异步方法完成,可以通过 CancellationToken 取消该任务,CancellationToken 是一个struct,通常使用 CancellationTokenSource 来创建 CancellationToken,因为CancellationTokenSource 有一些列的[方法]用于我们取消任务而不用去操作CancellationToken 结构体。
CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken ct = cts.Token;
然我改造下方法,将 CancellationToken 传递到异步方法,cts.CancelAfter(3000)
3秒钟后取消任务,我们监听CancellationToken 如果 IsCancellationRequested==true
则直接返回 。
static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken ct = cts.Token; cts.CancelAfter(3000); Console.WriteLine(DateTime.Now + " start"); AsyncAwaitTest(ct); Console.WriteLine(DateTime.Now + " end"); Console.ReadKey(); } public static async Task AsyncAwaitTest(CancellationToken ct) { Console.WriteLine("test start"); await Task.Delay(5000); Console.WriteLine(DateTime.Now + " cancel"); if (ct.IsCancellationRequested) { return; } //ct.ThrowIfCancellationRequested(); Console.WriteLine("test end"); }
因为我们是手动通过代码判断状态结束异步,所以即使在3秒后就已经结束了任务,但是await Task.Delay(5000)
任然会等待5秒执行完。还有一种方式就是我们不判断是否取消,直接调用ct.ThrowIfCancellationRequested()
给我们判断,这个方法如果,但是任然不能及时结束。这个时候我们还有另外一种处理方式,就是将CancellationToken 传递到 await 的异步API方法里,可能会立即结束,也可能不会,这个要取决异步实现。
public static async Task AsyncAwaitTest(CancellationToken ct) { Console.WriteLine("test start"); //传递CancellationToken 取消 await Task.Delay(5000,ct); Console.WriteLine(DateTime.Now + " cancel"); //手动处理取消 //if (ct.IsCancellationRequested) { // return; //} //调用方法处理取消 //ct.ThrowIfCancellationRequested(); Console.WriteLine("test end"); }
6、注意项
在异步方法里面不要使用 Thread.Sleep 方法,有两种可能:
1、Sleep在 await 之前,则会直接阻塞调用方线程等待Sleep。
2、Sleep在 await 之后,但是 await 执行在调用方的线程上也会阻塞调用方线程。
所以我们应该使用 Task.Delay 用于等待操作。那为什么我上面的 Task.Run 里面使用了 Thread.Sleep呢,因为 Task.Run 是显示请求在独立线程上运行,所以我知道这里写不会阻塞调用方,上面我只是为了演示,所以不建议用。
这篇关于.net 温故知新:【5】异步编程 async await的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-03-01沐雪多租宝商城源码从.NetCore3.1升级到.Net6的步骤
- 2024-11-18微软研究:RAG系统的四个层次提升理解与回答能力
- 2024-11-15C#中怎么从PEM格式的证书中提取公钥?-icode9专业技术文章分享
- 2024-11-14云架构设计——如何用diagrams.net绘制专业的AWS架构图?
- 2024-05-08首个适配Visual Studio平台的国产智能编程助手CodeGeeX正式上线!C#程序员必备效率神器!
- 2024-03-30C#设计模式之十六迭代器模式(Iterator Pattern)【行为型】
- 2024-03-29c# datetime tryparse
- 2024-02-21list find index c#
- 2024-01-24convert toint32 c#
- 2024-01-24Advanced .Net Debugging 1:你必须知道的调试工具