• C# 中 async 和 await 的基本使用


    C# 中 async 和 await 的基本使用

    前言

    经常在 C# 的代码中看到以 Async 结尾的方法,大概知道意为异步方法,但不知道怎么使用,也不知道如何定义。

    对于“同步”、“异步”、“阻塞”、"非阻塞"这几个概念还是比较清楚的。同步是指等待方法的执行完成;异步是指设置方法执行后继续其它操作,通过回调的方式对结果进行其它操作;阻塞是指执行到这一步就不往后了,直到执行完成;非阻塞是指执行这一步时,还可以进行其它操作。

    这两组概念其实是讲的一个东西,只是针对的方向有些许区别(一个强调是否立即返回,一个强调是否继续往后)

    对于 C# 中的 async 和 await,可以这么简单理解:async 告诉 runtime,这个函数可以异步去执行以提高效率。await 则告诉 runtime,真正耗时的是在我这个关键字后面的操作。

    本文仅希望在使用的层面验证,对于原理以及是否新开线程等,由于能力有限,暂不深入

    思路与实验

    对于本地环境而言,读取大文件是比较耗时的操作之一。因此先写一个读取文件的操作,再用 async 和 await 的方法将其包裹,以探究这两个关键字的使用(为了模拟执行一番后得到最后的结果,我们返回二进制文件的最后一个字节所代表的数字)

    1. 初步代码,同步调用耗时方法

    using System;
    using System.IO;
    
    namespace AsyncAwaitTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                DateTime time = DateTime.Now;
                byte targetNum;
    
                Console.WriteLine("模拟执行其它操作,用 A 表示");
    
                targetNum = ReadLargeFile();    // 为体现同步异步区别,执行三遍
                targetNum = ReadLargeFile();
                targetNum = ReadLargeFile();
                Console.WriteLine("最后一个字节所代表的数字为:" + targetNum);
    
                Console.WriteLine("模拟执行其它操作,用 B 表示");
                Console.WriteLine("耗时为:" + (DateTime.Now - time).Seconds);
                Console.ReadLine();
            }
            
            /// <summary>
            ///  读取大文件(耗时方法)
            /// </summary>
            /// <returns></returns>
            private static byte ReadLargeFile()
            {
                const int BUFFER_SIZE = 4096;
    
                FileStream fileStream = new FileStream(
                                                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Windows10.iso"), 
                                                FileMode.Open, FileAccess.Read, FileShare.Read);    // 在此处设置允许共享
                byte[] buffer = new byte[BUFFER_SIZE];
                int readOutCount = 0, lastReadOutCount = 0;
                while ((readOutCount = fileStream.Read(buffer, 0, BUFFER_SIZE)) != 0)
                {
                    lastReadOutCount = readOutCount;
                }
    
                return buffer[lastReadOutCount - 1];
            }
        }
    }
    

    可以看出,耗时约 10 s。

    2. 使用异步关键字包裹同步方法

    新增函数 AsyncCallReadLargeFile,并修改 main 函数中的调用。通过查阅资料可以得知,Task 类的 Result 方法在执行时会阻塞

    using System;
    using System.IO;
    using System.Threading.Tasks;
    
    namespace AsyncAwaitTest
    {
        class Program
        {
            static void Main(string[] args)
            {
    	    ......
    
                Console.WriteLine("模拟执行其它操作,用 A 表示");	// 执行顺序 1
    
                Task<byte> t1 = AsyncCallReadLargeFile();    // 为体现同步异步区别执行三遍
                Task<byte> t2 = AsyncCallReadLargeFile();
                Task<byte> t3 = AsyncCallReadLargeFile();
    
    	    Console.WriteLine("模拟执行其它操作,用 C 表示"); // 执行顺序 3 或 4
    
                targetNum = t1.Result;
                targetNum = t2.Result;
                targetNum = t3.Result;
    
                Console.WriteLine("最后一个字节所代表的数字为:" + targetNum);	// 执行顺序 6
    
                Console.WriteLine("模拟执行其它操作,用 B 表示");	// 执行顺序 7
                
    	    ......
            }
    
            /// <summary>
            /// 使用异步关键字包裹同步方法
            /// </summary>
            /// <returns></returns>
            private static async Task<byte> AsyncCallReadLargeFile()
            {
    	    Console.WriteLine("模拟执行异步子方法,用 a 表示");		// 执行顺序 2	
    	
                byte result = await Task.Run(ReadLargeFile);
    
    	    Console.WriteLine("模拟执行异步子方法,用 b 表示");		// 执行顺序 5
                return result;
            }
    
            /// <summary>
            /// 读取大文件(耗时方法)
            /// </summary>
            /// <returns></returns>
            private static byte ReadLargeFile()
            {
    	    Console.WriteLine("读取文件");	// 执行顺序 3 或 4
                ......
            }
        }
    }
    
    

    小结:通过耗时可以明显看出:
    (1)我们的异步方法确实是以异步的方式执行了(对同一文件进行三个异步读操作,耗时没有叠加)
    (2)大致的执行顺序如代码注释中所示,也即,使用 await 时,确实等待执行完成当前后才会执行异步函数中后续的方法
    (3)即使在异步函数中,未用 await 修饰的方法也是同步执行的(通过截图无法看出,但通过观察代码输出可以看出)

    其它一些思考

    1. 异步的方法最终会由同步方法调用

    这句话看上去有点绝对了,但确实是这个道理。从写法上:写函数时,有 async 就必须有 await(否则会警告,并且以同步方式执行),有 await 就必须有 async(否则会报错),而异步函数必须要使用这两个成对出现的关键字。从道理上:异步方法就是来解决同步方法顺序执行过于循规蹈矩问题的,没有同步方法的调用怎么会有这些问题呢?

    2. async,await 和 Task 什么关系

    尝试过这一种写法:

    /* 错误写法 */
    private static async byte AsyncCallReadLargeFile()
    {
        return await AsyncCallReadLargeFile();
    }
    

    会有如下错误提示:

    错误 CS1061 “byte”未包含“GetAwaiter”的定义,并且找不到可接受第一个“byte”类型参数的可访问扩展方法“GetAwaiter”(是否缺少 using 指令或程序集引用?)

    似乎可以认为,只有返回的类型包含 GetAwaiter 的定义,才能被当作异步函数来调用。最常见的只有 Task 包含这个方法。想到之前看到过,async 修饰的函数,返回类型只能是 void, Task, Task

    3. 异步方法的返回

    在 AsyncCallReadLargeFile 函数中,虽然签名中返回类型是 Task<byte> ,但我们实际上只返回了 byte 类型,并没有 Task。我的理解是对于 async 修饰的异步方法,返回的类型会自动被包装成 Task 的泛型类型。

    参考

    深入理解async和await的作用及各种适用场景和用法(旧,详见最新两篇)

    C# 彻底搞懂async/await

    (这两篇都很全面,受益匪浅)

    朝夕教育 bilibili 视频

  • 相关阅读:
    Python 7步机器学习
    STL容器-- map and multimap 用法
    STL容器-- forward_list 用法
    STL容器-- fixed-size array 用法
    STL容器-- deque 用法
    网站502与504错误分析
    git将一个分支的某个文件合并到当前分支
    阿里云RDS上用mysqldump导入导出
    mysqldump具体应用实例
    mysql的导入导出工具mysqldump命令详解
  • 原文地址:https://www.cnblogs.com/battor/p/csharp_async_await_simple_use.html
Copyright © 2020-2023  润新知