使用 async 和 await 关键字的好处

Posted

技术标签:

【中文标题】使用 async 和 await 关键字的好处【英文标题】:Benefits of using async and await keywords 【发布时间】:2015-05-04 16:01:07 【问题描述】:

我是 C# 中使用异步方法的新手。我读过这些关键字asyncawait 有助于通过异步一些方法使程序更具响应性。我有这个sn-p:

第一道

    public static void Main()
    
        Console.WriteLine("Hello!! welcome to task application");
        Console.ReadKey();
        Task<string> ourtask = Task.Factory.StartNew<string>(() =>
        
            return "Good Job";
        );
        ourtask.Wait();
        Console.WriteLine(ourtask.Result);
        Console.ReadKey();
    

第二种方式

 public static void Main()
        
            Launch();
        
        public static async void Launch()
        
            Console.WriteLine("Hello!! welcome to task application");
            Console.ReadKey();
            Console.WriteLine(await GetMessage());
            Console.ReadKey();
        

        public static Task<string> GetMessage()
        
            return Task.Factory.StartNew<string>(() =>
                
                    return "Good Job";
                );
        

我需要知道:

    这两种实现有区别吗(在并行概念上)?

    如果我可以创建一个任务并等待它完成,那么使用 asyncawait 关键字有什么好处?

【问题讨论】:

异步和并行是两个不同的东西,尽管您可以通过异步(以及其他方式)实现并行(在某些情况下),但它们是不同的概念,不应混淆。 你应该花一些时间阅读一些关于这些概念的教程/资源,因为它们是非常复杂的主题,有很多错综复杂的东西;解释有关它们的所有内容很好超出了 SO 问题的范围,更不用说评论了。整本书都是关于这个主题的。 @ErikFunkenbusch 异步代码严格地与可伸缩性无关。这只是一方面。 平行性是当您雇用两名厨师时,一名煮鸡蛋,一名烤面包。异步是当一个厨师检查鸡蛋,然后检查吐司,然后上鸡蛋,然后上吐司。异步可以是并行的,但不需要并行。 为了更清楚地定义差异,无需类比厨师:并行性是指同时完成两项工作。异步是指你必须等待某事,而你决定在等待时做其他工作。并行是实现异步的一种技术,但它不是唯一的。在您的示例代码中,您通过说“我希望同步等待此异步操作”来击败异步的好处;这就是Wait 调用的含义。 【参考方案1】:

假设您有一个边境检查站。每辆车都可以一一通过,让海关检查他们的车,看看他们是否没有走私任何比利时巧克力。

现在假设您在您的大众甲壳虫中排成一列,而您之前是一辆 24 轮的巨型卡车。你现在被困在这个庞然大物后面很长一段时间,直到海关完成所有搜索,然后他们才能移动到你身边,他们基本上只需要拍拍你就可以了。

为了打击这种效率,我们边境巡逻的好朋友有个主意,安装了第二个检查站。现在他们可以通过两倍的人,你可以直接拿走那个人,而不是在怪物卡车后面等待!

问题解决了,对吧?不完全是。他们忘记开辟通往该检查站的第二条道路,因此所有车辆仍然必须通过单车道,导致卡车仍然挡住甲壳虫。

这与您的代码有何关系?很简单:你也在做同样的事情。

当您创建一个新的Task 时,您实际上是在创建第二个检查点。但是,当您现在使用.Wait() 同步阻止它时,您是在强迫所有人走那条路。

在第二个示例中,您使用await 创建第二条道路并允许您的汽车与卡车同时处理。

【讨论】:

这个例子不能很好地代表这两种情况,也不能回答所提出的实际问题。这两种情况都不会涉及任何并行性,这就是您要在此处描述的内容。您的示例没有解释(即使以类比的形式)什么是异步或为什么有人可能想要使用它(类比完全涵盖了同步操作)。这个答案根本没有技术价值。它无助于解释实际发生的事情。显然,没有意义的有趣故事是所有人都感兴趣的。 @Servy:他的问题中的第一个项目符号询问了两个示例之间的实现差异,这就是我在这里描述的内容。如果我的类比和手头的问题之间存在技术差异,那么我想知道这样我可以改进它并从中学习,但是我看到它的方式我确实回答了这个问题。仅仅因为 cmets 对异步性与并行性感到困惑,并不意味着这是最重要的方面。 所以如果您已经解释了这两个示例之间的差异,您将成功回答所提问题的一小部分,但仍有大部分问题未得到回答。当然,您根本没有解释这两个程序之间的区别。仅举一个问题,第二个程序安排一些工作在后台线程中发生,然后结束该进程并在不做任何事情的情况下将所有内容全部关闭,仅举出它们之间的几十个差异之一。 第二个程序有错误。我很确定它不是以这种方式工作的设计目标。 (将Launch() 更改为Launch().Wait())我不理解在不解释原因或提供更好答案的情况下说答案“不好”的想法。 @Servy 为什么不发布一个更好的答案,而不是花费大量精力批评这个?【参考方案2】:

我将尝试直接回答问题:

    您的示例都没有(有效地)涉及任何并行性。我看到它们之间的两个主要区别:1)第一个示例将阻塞一个线程,而任务在第二个线程上运行,这是没有意义的,2)第二个示例将提前退出。一旦遇到await,控制立即返回Main(),由于您没有等待从Launch()返回的任务完成,您的程序将在此时退出。

    与等待任务完成相比,使用asyncawait 的好处是await 在任务运行时不会阻塞当前线程。在后台,只要编译器遇到await,它就会有效地将该方法的其余部分重写为将在任务完成时调用的回调。这可以释放当前线程在任务运行时执行其他操作,例如响应客户端应用程序中的用户输入或服务 Web 应用程序中的其他请求。

坦率地说,这不是展示async/await 好处的好例子。您基本上是在说您想做受 CPU 限制的工作,并且在完成该工作之前您不想做任何其他事情。您也可以同步执行此操作。异步在执行 I/O 密集型工作时非常出色,例如通过网络进行调用(使用正确实现的异步库,例如 HttpClient),因为您不只是像第二个示例中那样将一个线程换成另一个线程; there literally is no thread 被 I/O 绑定工作消耗。

正如其他人所暗示的,并行性完全是另一个主题。虽然async/await 可能是帮助您实现它的有用结构,但涉及的内容更多,在我看来,您最好在“继续”之前牢牢掌握线程释放的好处到并行。

正如其他人所暗示的那样,这是一个很大的话题,我强烈建议您查看一些很棒的资源。由于我已经引用了 Stephen Cleary 的博客,因此我将继续对其进行完整介绍 - 他的 async/await intro 和后续帖子是该主题的出色入门。

【讨论】:

【参考方案3】:

异步/等待编程有两个主要好处

1- 非阻塞编程

当您有不需要阻止执行的长时间运行的操作时。在这种情况下,您可以在等待长时间运行的任务结果的同时执行其他工作。

假设我们有两个程序流,它们可以并行工作而不会相互阻塞。

示例: 假设我们需要记录每个出现的错误,但同时这不应该阻塞流程,所以在这种情况下我们可以同时记录和返回消息。

2- async/await 编程中线程管理的好处

我们知道,在正常编程(阻塞)中,每一行代码都会阻塞它之后的所有内容,直到它完成进程,即使我们有不同的流程(两个流程没有任何依赖关系)。 但是在 async/await 编程中,应用程序不会阻塞这个线程,换句话说,他们会释放它来做另一个工作,当函数完成工作时,任何空闲线程都会处理响应。

C# async and await: Why Do We Need Them?

【讨论】:

【参考方案4】:

async / await 清理大量复杂的代码,这些代码会过度使用 Task.ContinueWith.ContinueWith.ContinueWith 等等。

从编码的角度来看,Task.ContinueWith 的可视化、调试和维护要困难得多,包括必须附带的相关异常处理。

所以,await 出现了,给了我们这个

    public static void Main()
    
        Launch();
    
    public static async void Launch()
    
        Console.WriteLine("Hello!! welcome to task application");
        Console.ReadKey();
        Console.WriteLine(await GetMessage());
        Console.ReadKey();
    

    public static Task<string> GetMessage()
    
        return Task.Factory.StartNew<string>(() =>
            
                return "Good Job";
            );
    

这几乎等同于:

    public static void Main()
    
        Launch();
    
    public static async void Launch()
    
        Console.WriteLine("Hello!! welcome to task application");
        Console.ReadKey();
        return  Task.Factory.StartNew(() => GetMessage())
            .ContinueWith((t) => 
                  
                     Console.WriteLine(t.Result)
                     Console.ReadKey();
                  );
    

    public static Task<string> GetMessage()
    
        return Task.Factory.StartNew<string>(() =>
            
                return "Good Job";
            );
    

您可以从示例中看到 GetMessage() 之后的所有内容都包含在 ContinueWith 中,但是该方法在创建后立即返回任务。所以它正在返回调用方法。

这里我们需要等待那个Task,否则程序会继续退出:

Launch().Wait();

不必编写 ContinueWith() 意味着我们的代码变得更具可读性,尤其是当我们必须在一个方法中将多个 await 块链接在一起时,它会“读取”得很好。

同样如前所述,await 示例处理了更好的异常处理,否则我们将不得不使用 TPL 方法来处理异常,这也会使代码库过于复杂。

关于您的两个示例,它们并不真正等效,因此您无法真正判断一个。但是,async/await 等价于构造Tasks/ContinueWith。

我将 async/await 视为 TPL 演变为实际语言本身。一种语法糖。

【讨论】:

【参考方案5】:

使用border checkpoint analogy,我会说:

同步边境员工

您有 4 条传入车道和 2 名警官。处理即将到来的巨型卡车的警官杰克不知道巨型卡车的确切规则,所以他打电话给办公室。 现在,如果他同步工作,他会一直在电话上等待回复。所以他无法处理其他任何事情 - 他的同事 Mary 必须这样做。

幸运的是,玛丽并行工作,因此她可以同时处理 3 条畅通的车道。然而,因为她也同步工作,她一次只能处理一辆车。 因此,当她必须检查边车上有猎豹的摩托车的规则时,她必须打电话给总公司,并在电话中等待答案。

现在我们有两条通道被挂起的作业阻塞,还有两条通道因为没有员工而阻塞。

异步边界员工

现在,如果 Jack 异步工作,他将不会等待响应。相反,他挂断了电话,走到第二条车道,处理另一辆车,同时等待总公司回电话。 因为第二车道是一位非常紧张的女士,口吃,这需要他相当长的时间。

但幸运的是,玛丽现在也可以异步工作,当她完成第二辆车的处理(或暂停,因为她必须检查猎豹)时,她可以在怪物卡车上接听办公室的回电。这样她就可以完成怪物卡车的处理了。 但当然,怪物卡车并没有在她完成后立即消失 - 司机已经记录了他在检查站花费的时间。幸运的是,Mary 仍然并行工作,因此她可以开始处理另一条车道上的汽车。

费用

然后有一天,火人节开始了,很多不寻常的车辆抵达边境。这些都需要给办公室打很多电话,因此阻塞了所有 4 条车道。所以杰克和玛丽只能坐等回电,而车辆的队伍还在增加。

幸运的是,这个地区的土地很便宜,所以他们的老板决定再增加 4 条车道。虽然这些额外的通道中的一些也被阻塞等待办公室的回电,但至少杰克和玛丽保持忙碌,不必坐下来等待电话。他们的老板当然可以考虑雇佣一些额外的员工来减少交通拥堵,但他知道他们需要住房和培训,而且节日很快就会结束,所以他就这样走了……

回顾

倾向于 ASP.Net:

通道(例如连接)很便宜,并且可以轻松扩展(您可能应该为异步增加 IIS 中的连接限制和队列大小,例如,请参阅下面的介绍参考) 员工(线程)更昂贵(例如,参见下面的介绍-参考)) 工作的“慢”部分(例如 I/O 或远程 http-requests)不应阻止昂贵的员工(即应该使用异步) 注意:工作可能会更换员工(双关语),因此不要将数据与您的员工一起保存。例如。 Jack 不应该把 Monstertruck-papers 放在他的口袋里,因为 Mary 可能会进一步处理它——他应该把它归还,或者将它保存在依赖于工作的存储中(不要将数据存储在线程中,而是存储在 HttpContext 中)

文学

FWIW:我喜欢这个 technical introduction 来自 2014 年的 Stephen Cleary。

【讨论】:

以上是关于使用 async 和 await 关键字的好处的主要内容,如果未能解决你的问题,请参考以下文章

教你正确打开async/await关键字的使用

python 3.5 中的 async/await 关键字是不是受到 C# 中的 async/await 的启发? [关闭]

Async和Await 异步方法

异步编程之Async,Await和ConfigureAwait的关系

C# 中的Async 和 Await 的用法详解

.NET Web应用中为什么要使用async/await异步编程