zoukankan      html  css  js  c++  java
  • .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 是显示请求在独立线程上运行,所以我知道这里写不会阻塞调用方,上面我只是为了演示,所以不建议用。

  • 相关阅读:
    Javascript高级篇-Function对象
    Object类、instanceof
    [一]Head First设计模式之【策略模式】(鸭子设计的优化历程)
    匿名内部类
    设计模式之单例模式
    长江商业评论读书笔记
    [转]Freemarker数据类型转换
    面向对象编程——概论(一)
    IP地址处理模块IPy
    系统性能模块psutil
  • 原文地址:https://www.cnblogs.com/SunSpring/p/15166143.html
Copyright © 2011-2022 走看看