zoukankan      html  css  js  c++  java
  • Async&Await

        • 1.什么是异步
        • 2.async/await 结构
        • 3.什么是异步方法
          • 3.1 异步返回类型
          • 3.2 控制流
            • 3.2.1 结构流程
            • 3.2.2 运行机制
          • 3.3 await 表达式
          • 3.4 How 取消异步操作

    1.什么是异步

    启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序资源的集合。 在进程内部,有称为线程的内核对象,它代表的是真正的执行程序。系统会在 Main 方法的第一行语句就开始线程的执行。

    线程:
    ①默认情况,一个进程只包含一个线程,从程序的开始到执行结束;
    ②线程可以派生自其它线程,所以一个进程可以包含不同状态的多个线程,来执行程序的不同部分;
    ③一个进程中的多个线程,将共享该进程的资源;
    ④系统为处理器执行所规划的单元是线程,而非进程。

    一般来说我们写的控制台程序都只使用了一个线程,从第一条语句按顺序执行到最后一条。但在很多的情况下,这种简单的模型会在性能或用户体验上不好。
    例如:服务器要同时处理来自多个客户端程序的请求,又要等待数据库和其它设备的响应,这将严重影响性能。程序不应该将时间浪费在响应上,而要在等待的同时执行其它任务!
    在异步程序中,代码不需要按照编写时的顺序执行。这时我们需要用到 C# 5.0 引入的 async/await 来构建异步方法。

    我们先看一下不用异步的示例:

    class Program
        {
            //创建计时器
            private static readonly Stopwatch Watch = new Stopwatch();
    
            private static void Main(string[] args)
            {
                //启动计时器
                Watch.Start();
    
                const string url1 = "http://www.cnblogs.com/";
                const string url2 = "http://www.cnblogs.com/liqingwen/";
    
                //两次调用 CountCharacters 方法(下载某网站内容,并统计字符的个数)
                var result1 = CountCharacters(1, url1);
                var result2 = CountCharacters(2, url2);
    
                //三次调用 ExtraOperation 方法(主要是通过拼接字符串达到耗时操作)
                for (var i = 0; i < 3; i++)
                {
                    ExtraOperation(i + 1);
                }
    
                //控制台输出
                Console.WriteLine($"{url1} 的字符个数:{result1}");
                Console.WriteLine($"{url2} 的字符个数:{result2}");
    
                Console.Read();
            }
    
            /// <summary>
            /// 统计字符个数
            /// </summary>
            /// <param name="id"></param>
            /// <param name="address"></param>
            /// <returns></returns>
            private static int CountCharacters(int id, string address)
            {
                var wc = new WebClient();
                Console.WriteLine($"开始调用 id = {id}{Watch.ElapsedMilliseconds} ms");
    
                var result = wc.DownloadString(address);
                Console.WriteLine($"调用完成 id = {id}{Watch.ElapsedMilliseconds} ms");
    
                return result.Length;
            }
    
            /// <summary>
            /// 额外操作
            /// </summary>
            /// <param name="id"></param>
            private static void ExtraOperation(int id)
            {
                //这里是通过拼接字符串进行一些相对耗时的操作
                var s = "";
    
                for (var i = 0; i < 6000; i++)
                {
                    s += i;
                }
    
                Console.WriteLine($"id = {id} 的 ExtraOperation 方法完成:{Watch.ElapsedMilliseconds} ms");
            }
        }
    

    执行结果:
    image

    【备注】一般来说,直接拼接字符串是一种比较耗性能的手段,如果对字符串拼接有性能要求的话应该使用 StringBuilder。
    【注意】每次运行的结果可能不同。不管哪次调试,绝大部分时间都浪费前两次调用(CountCharacters 方法),即在等待网站的响应上。
    根据执行结果所画的时间轴:
    image
    有人曾幻想着这样提高性能的方法:在调用 A 方法时,不等它执行完,直接执行 B 方法,然后等 A 方法执行完成再处理。 C# 的 async/await 就可以允许我们这么弄。

    class Program
        {
            //创建计时器
            private static readonly Stopwatch Watch = new Stopwatch();
    
            private static void Main(string[] args)
            {
                //启动计时器
                Watch.Start();
    
                const string url1 = "http://www.cnblogs.com/";
                const string url2 = "http://www.cnblogs.com/liqingwen/";
    
                //两次调用 CountCharactersAsync 方法(异步下载某网站内容,并统计字符的个数)
                Task<int> t1 = CountCharactersAsync(1, url1);
                Task<int> t2 = CountCharactersAsync(2, url2);
    
                //三次调用 ExtraOperation 方法(主要是通过拼接字符串达到耗时操作)
                for (var i = 0; i < 3; i++)
                {
                    ExtraOperation(i + 1);
                }
    
                //控制台输出
                Console.WriteLine($"{url1} 的字符个数:{t1.Result}");
                Console.WriteLine($"{url2} 的字符个数:{t2.Result}");
    
                Console.Read();
            }
    
            /// <summary>
            /// 统计字符个数
            /// </summary>
            /// <param name="id"></param>
            /// <param name="address"></param>
            /// <returns></returns>
            private static async Task<int> CountCharactersAsync(int id, string address)
            {
                var wc = new WebClient();
                Console.WriteLine($"开始调用 id = {id}{Watch.ElapsedMilliseconds} ms");
    
                var result = await wc.DownloadStringTaskAsync(address);
                Console.WriteLine($"调用完成 id = {id}{Watch.ElapsedMilliseconds} ms");
    
                return result.Length;
            }
    
            /// <summary>
            /// 额外操作
            /// </summary>
            /// <param name="id"></param>
            private static void ExtraOperation(int id)
            {
                //这里是通过拼接字符串进行一些相对耗时的操作
                var s = "";
    
                for (var i = 0; i < 6000; i++)
                {
                    s += i;
                }
    
                Console.WriteLine($"id = {id} 的 ExtraOperation 方法完成:{Watch.ElapsedMilliseconds} ms");
            }
        }
    

    修改后的执行结果图:
    image
    根据加入异步后的执行结果画的时间轴:
    image
    我们观察时间轴发现,新版代码比旧版快了不少(由于网络波动的原因,很可能会出现耗时比之前长的情况)。这是由于 ExtraOperation 方法的数次调用是在 CountCharactersAsync 方法调用时等待响应的过程中进行的。所有的工作都是在主线程中完成的,没有创建新的线程。

    【改动分析】只改了几个细节的地方,直接展开代码的话可能看不出来,改动如下:
    image
    image
    ①从 Main 方法执行到 CountCharactersAsync(1, url1) 方法时,该方法会立即返回,然后才会调用它内部的方法开始下载内容。该方法返回的是一个 Task<int> 类型的占位符对象,表示计划进行的工作。这个占位符最终会返回 int 类型的值。
    ②这样就可以不必等 CountCharactersAsync(1, url1)
    方法执行完成就可以继续进行下一步操作。到执行 CountCharactersAsync(2, url2) 方法时,跟 ① 一样返回 Task<int> 对象。
    ③然后,Main 方法继续执行三次 ExtraOperation 方法,同时两次 CountCharactersAsync 方法依然在持续工作 。
    ④t1.Result 和 t2.Result 是指从 CountCharactersAsync 方法调用的 Task<int> 对象取结果,如果还没有结果的话,将阻塞,直有结果返回为止。


    2.async/await 结构

    先解析一下专业名词:
    同步方法:一个程序调用某个方法,等到其执行完成之后才进行下一步操作。这也是默认的形式。
    异步方法:一个程序调用某个方法,在处理完成之前就返回该方法。通过 async/await 我们就可以实现这种类型的方法。

    async/await 结构可分成三部分:
    (1)调用方法:该方法调用异步方法,然后在异步方法执行其任务的时候继续执行;
    (2)异步方法:该方法异步执行工作,然后立刻返回到调用方法;
    (3)await 表达式:用于异步方法内部,指出需要异步执行的任务。一个异步方法可以包含多个 await 表达式(不存在 await 表达式的话 IDE 会发出警告)。
    现在我们来分析一下示例:
    image


    3.什么是异步方法

    异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。
    语法分析:
    (1)关键字:方法头使用 async 修饰。
    (2)要求:包含 N(N>0) 个 await 表达式(不存在 await 表达式的话 IDE 会发出警告),表示需要异步执行的任务。
    (3)返回类型:只能返回 3 种类型(void、Task 和 Task<T>)。Task 和 Task<T> 标识返回的对象会在将来完成工作,表示调用方法和异步方法可以继续执行。
    (4)参数:数量不限,但不能使用 out 和 ref 关键字。
    (5)命名约定:方法后缀名应以 Async 结尾。
    (6)其它:匿名方法和 Lambda 表达式也可以作为异步对象;async 是一个上下文关键字;关键字 async 必须在返回类型前。
    异步方法的简单结构图:
    image

    3.1 异步返回类型

    现在先来简单分析一下这三种返回值类型:void、Task 和 Task<T>
    (1)Task<T>:调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task<T>。调用方法从 Task 的 Result 属性获取的就是 T 类型的值。

    private static void Main(string[] args)
            {
                Task<int> t = Calculator.AddAsync(1, 2);
    
                //一直在干活
    
                Console.WriteLine($"result: {t.Result}");
    
                Console.Read();
            }
    
    Program.cs
    
    internal class Calculator
    {
        private static int Add(int n, int m)
        {
            return n + m;
        }
    
        public static async Task<int> AddAsync(int n, int m)
        {
            int val = await Task.Run(() => Add(n, m));
    
            return val;
        }
    }
    

    (2)Task:调用方法不需要从异步方法中取返回值,但是希望检查异步方法的状态,那么可以选择可以返回 Task 类型的对象。不过,就算异步方法中包含 return 语句,也不会返回任何东西。

    private static void Main(string[] args)
            {
                Task t = Calculator.AddAsync(1, 2);
    
                //一直在干活
    
                t.Wait();
                Console.WriteLine("AddAsync 方法执行完成");
    
                Console.Read();
            }
    
    Program.cs
    
    internal class Calculator
    {
        private static int Add(int n, int m)
        {
            return n + m;
        }
    
        public static async Task AddAsync(int n, int m)
        {
            int val = await Task.Run(() => Add(n, m));
            Console.WriteLine($"Result: {val}");
        }
    }
    

    (3)void:调用方法执行异步方法,但又不需要做进一步的交互。

    private static void Main(string[] args)
            {
                Calculator.AddAsync(1, 2);
    
                //一直在干活
    
                Thread.Sleep(1000); //挂起1秒钟
                Console.WriteLine("AddAsync 方法执行完成");
    
                Console.Read();
            }
    
    Program.cs
    
    internal class Calculator
        {
            private static int Add(int n, int m)
            {
                return n + m;
            }
    
            public static async void AddAsync(int n, int m)
            {
                int val = await Task.Run(() => Add(n, m));
                Console.WriteLine($"Result: {val}");
            }
        }
    

    3.2 控制流

    3.2.1 结构流程

    异步方法的结构可拆分成三个不同的区域:
    (1)表达式之前的部分:从方法头到第一个 await 表达式之间的所有代码。
    (2)await 表达式:将被异步执行的代码。
    (3)表达式之后的部分:await 表达式的后续部分。
    image
    该异步方法执行流程:从await表达式之前的地方开始,同步执行到第一个 await,标识着第一部分执行结束,一般来说此时 await 工作还没完成。当await 任务完成后,该方法将继续同步执行后续部分。在执行的后续部分中,如果依然存在 await,就重复上述过程。
    当到达 await 表达式时,线程将从异步方法返回到调用方法。如果异步方法的返回类型为 Task 或 Task<T>,会创建一个 Task 对象,标识需要异步完成的任务,然后将 Task 返回来调用方法。
    image
    异步方法的控制流:
    ①异步执行 await 表达式的空闲任务。
    ②await 表达式执行完成,继续执行后续部分。如再遇到 await 表达式,按相同情况进行处理。
    ③到达末尾或遇到 return 语句时,根据返回类型可以分三种情况:

    • a.void:退出控制流。
    • b.Task:设置 Task 的属性并退出。
    • c.Task<T>:设置 Task 的属性和返回值(Result 属性)并退出。

    ④同时,调用方法将继续执行,从异步方法获取 Task 对象。需要值的时候,会暂停等到 Task 对象的 Result 属性被赋值才会继续执行。

    【难点】
    ①第一次遇到 await 所返回对象的类型。这个返回类型就是同步方法头的返回类型,跟 await 表达式的返回值没有关系。
    ②到达异步方法的末尾或遇到 return 语句,它并没有真正的返回一个值,而是退出了该方法。

    3.2.2 运行机制

    异步编程中最需弄清的是控制流是如何从方法移动到方法的。 下图可引导你完成该过程。
    image
    关系图中的数值对应于以下步骤。 
    1.事件处理程序调用并等待 AccessTheWebAsync 异步方法。 
    2.AccessTheWebAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。 
    3.GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 AccessTheWebAsync。 
    GetStringAsync 返回 Task<TResult>,其中 TResult 为字符串,并且 AccessTheWebAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。 
    4.由于尚未等待 getStringTask,因此,AccessTheWebAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。 
    5.DoIndependentWork 是完成其工作并返回其调用方的同步方法。 
    6.AccessTheWebAsync 已用完工作,可以不受 getStringTask 的结果影响。 接下来,AccessTheWebAsync 需要计算并返回该下载字符串的长度,但该方法仅在具有字符串时才能计算该值。 
    因此,AccessTheWebAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 AccessTheWebAsync 的方法。 AccessTheWebAsync 将 Task<int> 返回给调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。 
    备注

    如果 GetStringAsync(因此 getStringTask)在 AccessTheWebAsync 等待前完成,则控制会保留在 AccessTheWebAsync 中。 
    如果异步调用过程 (getStringTask) 已完成,并且 AccessTheWebSync 不必等待最终结果,则挂起然后返回到 AccessTheWebAsync 将造成成本浪费。 
    在调用方内部(此示例中的事件处理程序),处理模式将继续。 在等待结果前,调用方可以开展不依赖于 AccessTheWebAsync 结果的其他工作,否则就需等待片刻。 
    事件处理程序等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync

    7.GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 urlContents。 
    8.当 AccessTheWebAsync 具有字符串结果时,该方法可以计算字符串长度。
    然后,AccessTheWebAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。
    如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。
    在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

    3.3 await 表达式

    await 表达式指定了一个异步执行的任务。默认情况,该任务在当前线程异步执行。
    每一个任务就是一个 awaitable 类的实例。awaitable 类型指包含 GetAwaiter() 方法的类型。
    实际上,你并不需要构建自己的 awaitable,一般只需要使用 Task 类,它就是 awaitable。
    最简单的方式是在方法中使用 Task.Run() 来创建一个 Task。【注意】它是在不同的线程上执行方法。
    有以下示例:

    internal class Program
        {
            private static void Main(string[] args)
            {
                var t = Do.GetGuidAsync();
                t.Wait();
    
                Console.Read();
            }
    
    
            private class Do
            {
                /// <summary>
                /// 获取 Guid
                /// </summary>
                /// <returns></returns>
                private static Guid GetGuid()   //与Func<Guid> 兼容
                {
                    return Guid.NewGuid();
                }
    
                /// <summary>
                /// 异步获取 Guid
                /// </summary>
                /// <returns></returns>
                public static async Task GetGuidAsync()
                {
                    var myFunc = new Func<Guid>(GetGuid);
                    var t1 = await Task.Run(myFunc);
    
                    var t2 = await Task.Run(new Func<Guid>(GetGuid));
    
                    var t3 = await Task.Run(() => GetGuid());
    
                    var t4 = await Task.Run(() => Guid.NewGuid());
    
                    Console.WriteLine($"t1: {t1}");
                    Console.WriteLine($"t2: {t2}");
                    Console.WriteLine($"t3: {t3}");
                    Console.WriteLine($"t4: {t4}");
                }
            }
        }
    

    返回结果:
    image
    上面 4 个 Task.Run() 都是采用了 Task Run(Func<TReturn> func) 形式来直接或间接调用 Guid.NewGuid()。
    Task.Run() 支持 4 中不同的委托类型所表示的方法:Action、Func<TResult>、Func<Task> 和 Func<Task<TResult>>

    internal class Program
    {
        private static void Main(string[] args)
        {
            var t = Do.GetGuidAsync();
            t.Wait();
    
            Console.Read();
        }
    
        private class Do
        {
            public static async Task GetGuidAsync()
            {
                await Task.Run(() => { Console.WriteLine(Guid.NewGuid()); });   //Action
    
                Console.WriteLine(await Task.Run(() => Guid.NewGuid()));    //Func<TResult>
    
                await Task.Run(() => Task.Run(() => { Console.WriteLine(Guid.NewGuid()); }));   //Func<Task>
    
                Console.WriteLine(await Task.Run(() => Task.Run(() => Guid.NewGuid())));    //Func<Task<TResult>>
            }
        }
    }
    

    image

    3.4 How 取消异步操作

    CancellationToken 和 CancellationTokenSource 这两个类允许你终止执行异步方法。
    (1)CancellationToken 对象包含任务是否被取消的信息;如果该对象的属性 IsCancellationRequested 为 true,任务需停止操作并返回;该对象操作是不可逆的,且只能使用(修改)一次,即该对象内的 IsCancellationRequested 属性被设置后,就不能改动。
    (2)CancellationTokenSource 可创建 CancellationToken 对象,调用 CancellationTokenSource 对象的 Cancel 方法,会使该对象的 CancellationToken 属性 IsCancellationRequested 设置为 true。
    【注意】调用 CancellationTokenSource 对象的 Cancel 方法,并不会执行取消操作,而是会将该对象的 CancellationToken 属性 IsCancellationRequested 设置为 true。

    internal class Program
        {
            private static void Main(string[] args)
            {
                CancellationTokenSource source = new CancellationTokenSource();
                CancellationToken token = source.Token;
    
                var t = Do.ExecuteAsync(token);
    
                //Thread.Sleep(3000);   //挂起 3 //source.Cancel();    //传达取消请求
    
                t.Wait(token);  //等待任务执行完成
                Console.WriteLine($"{nameof(token.IsCancellationRequested)}: {token.IsCancellationRequested}");
    
                Console.Read();
            }
    
    
        }
    
        internal class Do
        {
            /// <summary>
            /// 异步执行
            /// </summary>
            /// <param name="token"></param>
            /// <returns></returns>
            public static async Task ExecuteAsync(CancellationToken token)
            {
                if (token.IsCancellationRequested)
                {
                    return;
                }
    
                await Task.Run(() => CircleOutput(token), token);
            }
    
            /// <summary>
            /// 循环输出
            /// </summary>
            /// <param name="token"></param>
            private static void CircleOutput(CancellationToken token)
            {
                Console.WriteLine($"{nameof(CircleOutput)} 方法开始调用:");
    
                const int num = 5;
                for (var i = 0; i < num; i++)
                {
                    if (token.IsCancellationRequested)  //监控 CancellationToken
                    {
                        return;
                    }
    
                    Console.WriteLine($"{i + 1}/{num} 完成");
                    Thread.Sleep(1000);
                }
            }
        }
    

    下图是不调用 Cancel() 方法的结果图,不会取消任务的执行。
    image
    下图在 3 秒后调用 Cancel() 方法取消任务的执行: image
    image

  • 相关阅读:
    [POJ1176]Party Lamps(DFS,规律)
    [POJ1598]Excuses, Excuses!(模拟)
    [POJ2192]Zipper(DFS,剪枝)
    [POJ2157]Maze(DFS)
    [POJ1950]Dessert(DFS)
    [HIHO1353]满减优惠(状压,枚举)
    [HIHO1177]顺子(暴力,枚举)
    [HIHO1152]Lucky Substrings(暴力,枚举)
    计蒜客 25985.Goldbach-米勒拉宾素数判定(大素数) (2018 ACM-ICPC 中国大学生程序设计竞赛线上赛 B)
    计蒜客 28206.Runway Planning (BAPC 2014 Preliminary ACM-ICPC Asia Training League 暑假第一阶段第一场 F)
  • 原文地址:https://www.cnblogs.com/jizhiqiliao/p/10649083.html
Copyright © 2011-2022 走看看