使用 async 和 await 关键字的好处

Benefits of using async and await keywords

我不熟悉 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 关键字有什么好处?

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

现在假设您在您的 Volkswagen Beetle 中排队,在那里您几乎无法进入,而您是一辆 24 轮巨型卡车。你现在被困在这个庞然大物后面很长一段时间,直到海关完成对这一切的搜索,然后他们才能转移到你身上,他们基本上只需要拍拍你就可以告诉你你可以走了。

为了打击这种效率,边境巡逻的好朋友灵机一动,设置了第二个检查站。现在他们可以通过两倍的人,你可以只带一个而不是在怪物卡车后面等待!

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

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

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

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

我会尝试直接回答问题:

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

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

坦率地说,这不是一个很好的例子来展示 async/await 的好处。你基本上是在说你想做 CPU 绑定的工作,并且在完成该工作之前你不想做任何其他事情。您也可以同步执行此操作。异步在做 I/O-bound 工作时真的很闪耀,例如通过网络进行调用(使用正确实现的异步库,例如 HttpClient), because you're not simply trading one thread for another as in your second example; there literally is no thread 被 I/O-bound 工作消耗。

正如其他人所暗示的,并行性完全是另一个话题。虽然 async/await 可以是有用的结构来帮助您实现它,但涉及的内容更多,在我看来,您最好先牢牢掌握无线程的好处"moving on" 并行。

正如其他人所暗示的,这是一个很大的话题,我强烈建议您查看那里的一些重要资源。由于我已经引用了 Stephen Cleary 的博客,所以我将继续完整地介绍它 - 他的 async/await intro 和后续帖子是关于该主题的优秀入门文章。

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 中,但是方法 returns 任务一经创建便立即生效。所以它正在返回调用方法。

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

Launch().Wait();

不必编写 ContinueWith() 意味着我们的代码变得更具可读性,特别是在我们必须在一个方法中将多个 await 块链接在一起的情况下,它会 "read" 很好。

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

关于你的两个例子,它们并不完全等同,所以你不能真正判断一个。但是,async/await等价于构造Tasks/ContinueWith。

我认为 async/await 是 TPL 向实际语言本身的演变。一种语法糖。

async/await 编程

我们有两个主要好处

1-非阻塞编程

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

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

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

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

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

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

border checkpoint analogy 一起,我会说:

同步边境员工

你有 4 条传入车道和 2 名军官。处理即将到来的巨型卡车的警官杰克不知道巨型卡车的确切规则,所以他打电话给办公室。 现在,如果他同步工作,他会留在 phone 上等待响应。所以他无法处理其他任何事情 - 他的同事玛丽将不得不这样做。

幸运的是,Mary 是并行工作的,所以她可以同时处理 3 个畅通的车道。然而,因为她也同步工作,所以她一次只能处理一辆车。 因此,当她必须检查边车上有猎豹的摩托车的规则时,她必须打电话给总办公室,并留在 phone 上寻求答案。

现在我们有两条通道被待处理的工作阻塞,还有两条通道因为没有员工而阻塞。

异步边界员工

现在,如果 Jack 异步工作 - 他将不会等待响应。相反,他挂断电话,走到第二条车道,处理另一辆车,同时等待总部 return 他的电话。 因为第二道是一个口吃的非常紧张的女士,这花了他一些时间。

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

费用

然后有一天,火人节开始了,许多奇特的车辆到达了边境。这些都需要打很多电话到办公室,因此阻塞了所有 4 条通道。所以杰克和玛丽只能坐着等return电话,而车辆的队伍越来越长。

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

回顾

倾向于ASP.Net:

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

文学

FWIW:我喜欢这个technical introduction from 2014 by Stephen Cleary