myesn

myEsn2E9

hi
github

C#: 异步编程与 async 和 await(Asynchronous programming with async and await)

Task 异步编程模型(TAP)为异步代码提供了一种抽象。我们可以像往常一样按照顺序编写代码,每个语句看起来都会在下一个语句开始之前完成 **。编译器会进行许多转换,因为其中一些语句可能会启动工作并返回表示正在进行的工作的 Task

这种语法的目标是使代码读起来像一连串语句,但执行顺序却复杂得多,它基于外部资源分配和任务完成时间。这类似于为包含异步任务的流程提供指令的方式。在本文中,将使用制作早餐的示例来演示 async 和 await 关键字如何使包含一系列异步指令的代码更易于理解。我们可以像以下列表一样编写指令来解释如何制作早餐:

  1. 倒一杯咖啡。
  2. 加热平底锅,然后煎两个鸡蛋。
  3. 煎三片培根。
  4. 烤两片面包。
  5. 在面包上涂上黄油和果酱。
  6. 倒一杯橙汁。

如果你有烹饪经验,你会异步执行这些指令。你会先开始加热平底锅准备煎鸡蛋,然后开始煎培根。你会把面包放进烤箱,然后开始煮鸡蛋。在整个过程的每个步骤中,你都会启动一个任务,然后转向那些准备好需要你处理的任务。

制作早餐是一个很好的异步工作而非并行工作的例子。一个人(或线程)可以处理所有这些任务。延续早餐的比喻,一个人可以通过在第一个任务完成之前开始下一个任务来异步制作早餐。烹饪过程会继续进行,无论是否有人在观察。一旦你开始加热平底锅准备煎鸡蛋,你就可以开始煎培根。一旦培根开始煎,你就可以把面包放进烤箱。

对于并行算法(Parallel algorithm),你需要多个厨师(或线程)。一个人负责煎鸡蛋,一个人负责煎培根,以此类推。每个人专注于自己的任务。每个厨师(或线程)会同步地被阻塞,等待培根煎熟需要翻面,或者面包弹出来

现在,我们考虑将这些指令写成 C# 语句:

namespace AsyncBreakfast
{
    // 这些类在此示例中是故意为空的。它们只是用于演示的标记类,不包含属性,并且没有其他作用。
    internal class Bacon { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("咖啡已准备好");

            Egg eggs = FryEggs(2);
            Console.WriteLine("鸡蛋已煎好");

            Bacon bacon = FryBacon(3);
            Console.WriteLine("培根已煎好");

            Toast toast = ToastBread(2);
            ApplyButter(toast);
            ApplyJam(toast);
            Console.WriteLine("面包已烤好");

            Juice oj = PourOJ();
            Console.WriteLine("橙汁已倒好");
            Console.WriteLine("早餐已准备好!");
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("倒橙汁");
            return new Juice();
![Untitled](ipfs://QmUUPaUCsngMZh4apLSSypnPovBtkymu2qhgSUUwz3JK8K)

        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("在面包上涂果酱");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("在面包上涂黄油");

        private static Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("将一片面包放入烤面包机");
            }
            Console.WriteLine("开始烤面包...");
            Task.Delay(3000).Wait();
            Console.WriteLine("从烤面包机中取出面包");

            return new Toast();
        }

        private static Bacon FryBacon(int slices)
        {
            Console.WriteLine($"将{slices}片培根放入平底锅");
            Console.WriteLine("煎培根的第一面...");
            Task.Delay(3000).Wait();
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("翻转一片培根");
            }
            Console.WriteLine("煎培根的第二面...");
            Task.Delay(3000).Wait();
            Console.WriteLine("将培根放在盘子上");

            return new Bacon();
        }

        private static Egg FryEggs(int howMany)
        {
            Console.WriteLine("预热煎蛋锅...");
            Task.Delay(3000).Wait();
            Console.WriteLine($"打开{howMany}个鸡蛋");
            Console.WriteLine("煮鸡蛋...");
            Task.Delay(3000).Wait();
            Console.WriteLine("将鸡蛋放在盘子上");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("倒咖啡");
            return new Coffee();
        }
    }
}

Untitled

计算机不像人类一样解读这些指令。计算机会在每个语句上阻塞,直到工作完成后才继续执行下一个语句。这样会导致早餐准备时间很长,并且有些食物在上桌前就已经变凉了。

如果想让计算机异步执行以上指令,必须编写异步代码。

这些问题对于我们现在编写的程序非常重要。当编写客户端程序时,你希望用户界面对用户输入有响应。在从网络下载数据时,您的应用程序不应该让手机出现卡顿。在编写服务器程序时,你不希望线程被阻塞。这些线程可能正在处理其他请求。在异步代码可替代的情况下使用同步代码会影响你以更低成本扩展程序的能力。你要为这些被阻塞的线程付出代价。

成功的现代应用程序需要异步代码。在没有语言支持的情况下,编写异步代码需要使用回调、完成事件或其他方式,这会掩盖代码的原始意图。同步代码的优点在于它按步骤执行,易于扫描和理解。传统的异步模型让你更多关注代码的异步特性,而不是代码的基本操作。

不要阻塞(block),而是 await#

前面的代码演示了一种糟糕的做法:编写同步代码来执行异步操作。按照写法,这段代码会阻止执行它的线程进行任何其他工作当任何任务正在进行时,它都不会被中断。这就好比你把面包放进面包机后盯着面包机。在面包片爆开之前,你不会理睬任何和你说话的人

让我们先更新这段代码,使线程在任务运行时不会阻塞。**await 关键字提供了一种非阻塞的方式来启动任务,然后在任务完成后继续执行 **。制作早餐代码的简单异步版本如下所示:

static **async Task** Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("咖啡已准备好");

    Egg eggs = await FryEggsAsync(2);
    Console.WriteLine("鸡蛋已煎好");

    Bacon bacon = await FryBaconAsync(3);
    Console.WriteLine("培根已煎好");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("面包已烤好");

    Juice oj = PourOJ();
    Console.WriteLine("橙汁已倒好");
    Console.WriteLine("早餐已准备好!");
}

在烹饪鸡蛋或培根时,这段代码不会阻塞。但这段代码不会启动任何其他任务。你还是会把面包片放进烤面包机,然后盯着它直到它爆开。但至少,你会对任何想引起你注意的人做出回应。在一家有多个订单的餐厅里,厨师可以在烹饪第一份早餐时开始另一份早餐。

现在,做早餐的线程在等待尚未完成的任务时不会被阻塞。对于某些应用程序来说,只需做出这样的改变即可。图形用户界面应用程序只需做出这样的改变,就能对用户做出响应。但是,在这种情况下,您需要的更多。你不希望每个组件任务都按顺序执行。最好是在等待前一个任务完成之前启动每一个组件任务。

并发启动任务(Start tasks concurrently)#

在许多场景中,你希望立即启动多个独立的任务。然后,当每个任务完成时,你可以继续进行其他已准备好的工作。在早餐的比喻中,这是如何更快地完成早餐的方法。你还可以同时完成所有事情。你会得到一份热腾腾的早餐。

System.Threading.Tasks.Task 和相关类型是你可以用来处理正在进行的任务的类。这使得你能够编写更接近制作早餐的代码。你会同时开始煮鸡蛋、培根和烤面包。当每个任务需要行动时,你会转向该任务,处理下一个动作,然后等待需要你注意的其他任务。

你会启动一个任务并保存代表该任务的 Task 对象。在处理每个任务的结果之前都要 await

让我们对早餐代码进行这些更改。第一步是将任务存储起来,以便在任务开始时进行操作,而不是等待任务:

Coffee cup = PourCoffee();
Console.WriteLine("咖啡已准备好");

Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("鸡蛋已煎好");

Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("培根已煎好");

Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("面包已烤好");

Juice oj = PourOJ();
Console.WriteLine("橙汁已倒好");
Console.WriteLine("早餐已准备好!");

上述代码不会让早餐更快准备好。任务在开始后立即被 await。接下来,你可以将对培根和鸡蛋的 await 语句移动到方法末尾,在上菜之前进行等待:

Coffee cup = PourCoffee();
Console.WriteLine("咖啡已准备好");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("面包已烤好");
Juice oj = PourOJ();
Console.WriteLine("橙汁已倒好");

Egg eggs = await eggsTask;
Console.WriteLine("鸡蛋已煎好");
Bacon bacon = await baconTask;
Console.WriteLine("培根已煎好");

Console.WriteLine("早餐已准备好!");

Untitled 1

这次采用异步准备早餐大约花了 20 分钟,之所以能节省时间,是因为有些任务是并发运行的。

前面的代码效果更好。你一次性启动所有的异步任务。只有在需要结果时才等待每个任务。前面的代码可能类似于一个向不同微服务发送请求并将结果合并到单个页面中的网络应用程序的代码。你会立即发出所有请求,然后等待所有任务完成并组合网页

组合任务(Composition with tasks)#

除了面包片之外,早餐的一切都同时准备就绪。制作面包片包括一个异步操作(烤面包),以及同步操作(加黄油和果酱)。更新这段代码可以说明一个重要的概念:

前面的代码展示了可以使用 TaskTask 对象来保存正在运行的任务。在使用结果之前,你需要 await 每个任务的完成。下一步是创建代表其他工作组合的方法。在供应早餐之前,需要等待烤面包的任务完成,然后再添加黄油和果酱。可以用以下代码表示这个工作:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

上述方法在其签名中有 async 修饰符。这向编译器发出信号,表明该方法包含一个 await 语句;它包含异步操作。该方法表示烤面包、加黄油和果酱的任务。该方法返回一个 Task,表示这三个操作的组合。现在主要的代码块变成了:

Coffee cup = PourCoffee();
Console.WriteLine("咖啡已准备好");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = MakeToastWithButterAndJamAsync(2);

Egg eggs = await eggsTask;
Console.WriteLine("鸡蛋已煎好");

Bacon bacon = await baconTask;
Console.WriteLine("培根已煎好");

Toast toast = await toastTask;
Console.WriteLine("面包已烤好");

Juice oj = PourOJ();
Console.WriteLine("橙汁已倒好");
Console.WriteLine("早餐已准备好!");

上述更改展示了处理异步代码的重要技巧。通过将操作分离到一个新方法中,并返回一个任务来组合任务。你可以选择何时等待该任务,也可以同时启动其他任务。

异步异常(Asynchronous exceptions)#

到目前为止,你默认假设所有这些任务都能成功完成。异步方法和同步方法一样,也会抛出异常。异步异常和错误处理的目标与一般的异步支持相同:你编写的代码应该读起来像一系列同步语句。当任务无法成功完成时,它们会抛出异常。在 await 已启动的任务时,客户端代码可以捕获这些异常。例如,假设烤面包机在做面包时着火了。可以通过修改 ToastBreadAsync 方法来模拟这种情况,代码如下:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("将一片面包放入烤面包机");
    }
    Console.WriteLine("开始烤面包...");
    await Task.Delay(2000);
    Console.WriteLine("着火啦!烤面包毁了!");
    throw new InvalidOperationException("烤面包机着火了");
    await Task.Delay(1000);
    Console.WriteLine("从烤面包机中取出面包");

    return new Toast();
}

作出这些更改后运行应用程序,你将看到与下面文本类似的输出结果:

....
培根已煎好
Unhandled exception. System.InvalidOperationException: 烤面包机着火了
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

你会注意到,在烤面包机着火和观察到异常之间,有很多任务已经完成。当异步运行的任务抛出异常时,该任务就会发生故障(faulted*)*。任务对象会在 Task.Exception 属性中保存抛出的异常。故障任务在 await 时会抛出异常。

有两个重要机制需要了解:异常如何存储在故障任务中,以及代码在等待故障任务时如何解包并重新抛出异常。

当异步运行的代码抛出异常时,异常会存储在 Task 中Task.Exception 属性是 System.AggregateException,因为在异步工作期间可能会抛出不止一个异常。抛出的任何异常都会添加到 AggregateException.InnerExceptions 集合中。如果 Exception 属性为 null,就会创建一个新的 AggregateException,而抛出的异常就是集合中的第一个 item。

故障任务最常见的情况是 Exception 属性正好包含一个异常。当代码 await 故障任务时,AggregateException.InnerExceptions 集合中的第一个异常会被重新抛出。这就是为什么本例的输出显示的是 InvalidOperationException 而不是 AggregateException提取第一个内部异常可使异步方法的工作尽可能与同步方法相似。当您的应用场景可能产生多个异常时,您可以检查代码中的 Exception 属性。

在继续之前,注释掉 ToastBreadAsync 方法中的这两行。你可不想再引起一场火灾:

Console.WriteLine("着火啦!烤面包毁了!");
throw new InvalidOperationException("烤面包机着火了");

高效地等待任务(Await tasks efficiently)#

在前面的代码中,通过使用 Task 类的方法可以改进一系列的 await 语句。其中一个 API 是 WhenAll,它可以在参数列表中的所有任务都完成时返回一个 Task,如下所示的代码所示:

await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

另一种方法是使用 WhenAny,它返回一个 Task<Task>,当其参数中的任何一个完成时,它就会完成。你可以等待返回的 Task,知道它已经完成。下面的代码展示了如何使用 WhenAny 来等待第一个完成的 Task,然后处理其结果。在处理完已完成的任务的结果后,你可以将该已完成任务从传递给 WhenAny 的任务列表中移除。

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == baconTask)
    {
        Console.WriteLine("Bacon is ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

在接近结尾的地方,你会看到一行代码 await finishedTask因为 await Task.WhenAny 并不等待已完成的任务。它等待的是 Task.WhenAny 返回的 TaskTask.WhenAny 的结果是已完成(或发生故障)的任务。你应该再次等待该任务,即使你知道它已经运行完毕。这样才能获取其结果,或确保导致其故障的异常被抛出。

在所有这些更改之后,代码的最终版本如下所示:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class Bacon { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var baconTask = FryBaconAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == baconTask)
                {
                    Console.WriteLine("bacon is ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Untitled 2

异步准备早餐的最终版本大约花了 15 分钟,因为某些任务并行运行,并且代码可以同时监视多个任务,只在需要时才采取行动。

这个最终版的代码是异步的。它更准确地反映了一个人做早餐的方式。将前面的代码与本文开头的第一个代码示例进行比较。从代码中仍然可以清晰地看出核心操作。你可以像阅读本文开头关于制作早餐的说明一样阅读这段代码。asyncawait 的语言特性提供了每个人遵循书面说明的翻译方法:尽可能启动任务,并且不阻塞等待任务完成。

参考#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.