使用async和await关键字的好处

15

我在使用C#中的异步方法方面还比较新。我读到这些关键字 asyncawait 可以通过将一些方法异步化来帮助使程序更加响应。我有以下代码片段:

第一种方式

    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";
                });
        }

我需要知道:

  1. 这两种实现方式(在并行概念上)有什么不同?

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


2
异步和并行是两个不同的概念,尽管您可以通过异步(还有其他方法)在某些情况下实现并行,但它们是独立的概念,不应混淆。 - Erik Funkenbusch
3
由于这些概念非常复杂且包含许多细节,建议您花些时间阅读一些教程/资源;解释所有内容已经远远超出了一个SO问题的范围,更不用说评论了。整本书都是关于这个主题的。 - Servy
1
@ErikFunkenbusch 当处理桌面应用程序时,完全有可能(并且很常见)希望拥有完全单线程的应用程序。只有在应用程序是异步的情况下才有可能实现这一点。它根本不涉及并行性。这与“扩展”无关。 - Servy
8
并行是当你雇用两个厨师,一个负责煮鸡蛋,另一个负责烤面包。异步是一个厨师检查鸡蛋,然后检查面包,再上菜鸡蛋,再上菜面包。异步可以是并行的,但不一定需要并行。 - Eric Lippert
5
为了更清晰地定义区别,不使用厨师的比喻:并行是指同时完成两项任务。异步是指在等待某些事情时,您决定做其他工作。并行是实现异步的一种技术,但它并不是唯一的技术。在您的示例代码中,“我希望同步等待此异步操作”打败了异步的好处;这就是“Wait”调用的含义。 - Eric Lippert
显示剩余14条评论
5个回答

68
假设有一个单一的边境检查站。每辆车都可以一个接着一个通过它,让海关人员查看他们的汽车,以查看他们是否走私了任何比利时巧克力。
现在假设你开着你的大众甲壳虫排队,你几乎挤不进去,在你前面是一辆24轮的怪物卡车。你现在被这个庞然大物困住了很长时间,直到海关搜查完所有东西才能继续前进,然后海关只需简单地搜身告诉你可以走了。
为了解决这个效率问题,我们的好朋友边防巡逻队有一个想法,安装了第二个检查站。现在他们可以通过两倍的人数,而且你只需要走那一个,而不用在怪物卡车后面等待!
问题解决了,对吧?并不完全是这样。他们忘记了创建通向该检查站的第二条道路,因此所有交通仍然必须经过单车道,结果是卡车仍然挡在甲壳虫后面。
这与您的代码有什么关系?非常容易:您正在做同样的事情。
当您创建一个新的 Task 实际上相当于创建第二个检查站。但是,当您现在使用 .Wait()同步阻止它时,您正在强制每个人都走这条单车道。
在第二个示例中,您使用的是 await ,它创建了第二条道路,并允许您的汽车与卡车同时处理。

4
这个例子并没有很好地代表两种情况,也没有回答实际问题。这两种情况都涉及不到任何并行性,而这正是你想在这里描述的。你的例子没有解释异步是什么,或者为什么有人想要使用它(比喻只覆盖完全同步操作)。这个回答根本没有技术价值,没有解释实际发生了什么。显然,人们只对没有实际价值但有趣的故事感兴趣。 - Servy
所以,如果你解释了这两个示例之间的差异,你就已经成功回答了问题的一小部分,但仍然有很多问题没有回答。当然,你根本没有解释这两个程序之间的区别。只是举一个问题,第二个程序安排了一些工作在后台线程中进行,然后结束进程并拆除所有内容而不做任何事情,这只是它们之间数十个差异之一。 - Servy
第二个程序有一个错误。我非常确定这不是它的设计目标。 (将 Launch() 更改为 Launch().Wait())我不明白仅仅说答案“不好”而没有解释为什么或提供更好的答案的想法。 - Grax32
27
@Servy为什么不发表一个更好的回答,而是花那么多精力批评这个回答呢? - Todd Menier
2
@ToddMenier 因为这个问题的答案非常复杂,即使写一本书也无法涵盖所有内容。这个问题太过宽泛,无法在SO上得到可回答的答案。这就是这个回答的主要问题之一,它甚至没有开始回答这个问题,而且充满了技术错误和错误信息。编写一个真正的答案几乎是不可能的。 - Servy
显示剩余3条评论

11

我将尝试直接回答这些问题:

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

  2. 使用 asyncawait 相对于等待任务完成的好处在于,await 在该任务运行时不会阻塞当前线程。在底层,每次编译器遇到 await 时,它实际上会将该方法的其余部分重写为在任务完成时将调用的回调函数。这释放了当前线程,使其在任务运行时可以做其他事情,例如在客户端应用程序中响应用户输入或在 Web 应用程序中服务其他请求。

坦白说,这不是一个很好的示例来展示 async/await 的好处。你基本上是说你想要做 CPU 绑定的工作,并且在该工作完成之前你不想做任何其他事情。你可能会同步地做到这一点。当执行面向 I/O 绑定的工作时,例如通过使用适当实现异步库的网络调用(如 HttpClient),异步操作确实非常出色。因为它不像第二个示例那样只是将一个线程换成另一个线程;就像 没有线程 被占用进行 I/O 绑定的操作。

正如其他人所提到的,并行性是另一个完全不同的主题。虽然async/await可以是有用的构造来帮助你实现它,但还涉及更多内容,我认为在“进入”并行之前,更好的方法是掌握释放线程的好处。

同时,正如其他人所提到的,这是一个大的主题,我强烈建议您查看一些伟大的资源。由于我已经提到了Stephen Cleary的博客,我将继续对其进行充分的推荐-他的async/await介绍和后续文章是该主题的优秀入门教程。


6
我们进行异步/等待编程的两个主要好处是: 1-非阻塞编程 当您有长时间运行的操作时,而不需要阻止执行。在这种情况下,您可以在等待长时间运行任务的结果时执行其他工作。
想象一下,我们有两个程序流,并且它们可以并行工作而不会相互阻塞。
例如: 假设我们需要记录每次出现的错误,但同时这不应阻止流程,因此在这种情况下,我们可以同时记录和返回消息。 2-异步/等待编程中的线程管理优势 我们知道,在正常编程(阻塞)中,每行代码都会阻止其后的所有内容,直到完成进程,即使我们具有不同的流(两个流没有任何依赖关系)。 但在异步/等待编程中,应用程序不会阻止此线程,换句话说,它们将释放它以执行其他工作,当函数完成工作时,任何空闲线程都将处理响应。
来源:C#异步和等待:为什么需要它们?

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中,但是该方法一旦创建就会立即返回任务。因此它返回给调用方法。
在这里,我们需要等待该任务,否则程序将继续退出:
Launch().Wait();

不需要编写ContinueWith()意味着我们的代码变得更易读,特别是在一个方法中链接多个await块的情况下,它会“读起来”很好。
另外,如前所述,更好的异常处理是通过< strong>await 示例处理的,否则我们将不得不使用TPL方法处理异常,这也可能使代码库过于复杂。
关于您提供的两个示例,它们并不真正等价,因此您不能仅仅根据一个来评判另一个。但是,async/await相当于构造任务/ContinueWith。
我认为异步/等待是TPL进化到实际语言本身的一种语法糖。

2

使用border checkpoint analogy的比喻,我会这样说:

异步边境员工

你有4个进口车道和2名官员。正在处理进入的怪物卡车的官员杰克不知道怪物卡车的确切规则,因此他打电话到办公室询问。 现在,如果他采用同步方式工作,他会一直等待回应。 因此,他无法处理其他任何事情 - 他的同事玛丽将不得不处理其他车道。

幸运的是,玛丽采用并行方式工作,因此她可以同时处理3个未阻塞的车道。 但是,由于她也是同步工作,因此她一次只能处理一个车辆。 因此,当她需要检查侧车上有猎豹的摩托车的规则时,她必须打电话给主办公室,并等待回答。

现在我们有两个车道被挂起的工作卡住了,另外两个车道因为没有员工而被堵塞。

异步边境员工

现在,如果杰克采用异步工作方式,他将不会占线等待回复。相反,他会挂断电话,前往第二条车道,并在等待总部的电话时处理另一辆车。 由于第二条车道上有位非常紧张并且口吃的女士,这需要花费相当长的时间。
但幸运的是,玛丽现在也采用异步工作方式,当她完成了第二辆车的处理(或因为她必须检查猎豹而暂停),她可以接听总部打来的关于怪物卡车的电话。这样她就能完成处理怪物卡车的工作。
但当然,怪物卡车在她完成后并没有立即离开——司机必须记录通过检查站所花费的时间。幸运的是,玛丽仍然在同时操作,所以她可以开始处理另一条车道中的车辆。

成本

然后有一天,燃人节庆祝活动开始了,许多不寻常的车辆到达边境。所有这些都需要大量向总部打电话,因此阻塞了所有4条车道。因此,杰克和玛丽只能坐在那里等待回电,而车辆队列则越来越长。
幸运的是,这个地区的土地很便宜,所以他们的老板决定增加4条车道。虽然其中一些额外的车道也被阻塞,等待办公室的回电,但至少杰克和玛丽保持忙碌,不必坐着等电话。当然,他们的老板可以考虑雇用一些额外的员工来减少交通拥堵,但他知道他们需要住房和培训,并且节日很快就会结束,所以他让事情保持原样...

总结

倾向于ASP.Net:

  • 车道(例如连接)便宜,并且可以轻松扩展(您应该增加IIS中的连接限制和队列大小,以进行异步处理,例如请参见下面的介绍-参考文献)
  • 员工(线程)更昂贵(例如,请参见下面的介绍-参考文献)
  • 作业的“慢”部分(例如I/O或远程http请求)不应阻塞您昂贵的员工(即应使用异步处理)
  • NB:作业可能会更改员工(双关语),因此不要将数据与员工一起保存。例如,Jack不应将怪物卡车文件放在口袋里,因为Mary可能会进一步处理它-他应该将其返回或将其保留在与作业相关的存储中(不要在线程内存储数据,而是在HttpContext中存储)

文献资料

FWIW:我喜欢Stephen Cleary于2014年发布的技术介绍


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接