C# 中基于任务的异步编程模型The Task asynchronous programming model in C#

基于任务的异步编程模型 (TAP) 提供了异步代码的抽象化。The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. 你只需像往常一样将代码编写为一连串语句即可。You write code as a sequence of statements, just like always. 就如每条语句在下一句开始之前完成一样,你可以流畅地阅读代码。You can read that code as though each statement completes before the next begins. 编译器将执行若干转换,因为其中一些语句可能会开始运行并返回表示正在运行中的 TaskThe compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.

这就是此语法的目标:支持读起来像一连串语句的代码,但会根据外部资源分配和任务完成时间以更复杂的顺序执行。That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. 这与人们为包含异步任务的流程给予指令的方式类似。It's analogous to how people give instructions for processes that include asynchronous tasks. 整篇文章将使用做早餐的指令示例来阐述 asyncawait 关键字如何使推断包含一系列异步指令的代码更为轻松。Throughout this article, you'll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. 你可能会写出与以下列表类似的指令来解释如何做早餐:You'd write the instructions something like the following list to explain how to make a breakfast:

  1. 倒一杯咖啡。Pour a cup of coffee.
  2. 加热平底锅,然后煎两个鸡蛋。Heat up a pan, then fry two eggs.
  3. 煎三片培根。Fry three slices of bacon.
  4. 烤两片面包。Toast two pieces of bread.
  5. 在烤面包上加黄油和果酱。Add butter and jam to the toast.
  6. 倒一杯橙汁。Pour a glass of orange juice.

如果你有烹饪经验,便可通过异步方式执行这些指令 。If you have experience with cooking, you'd execute those instructions asynchronously. 你会先开始加热平底锅以备煎蛋,接着再从培根着手。You'd start warming the pan for eggs, then start the bacon. 你可将面包放进烤面包机,然后再煎鸡蛋。You'd put the bread in the toaster, then start the eggs. 在此过程的每一步,你都可以先开始一项任务,然后将注意力转移到准备进行的其他任务上。At each step of the process, you'd start a task, then turn your attention to tasks that are ready for your attention.

做早餐是非并行异步工作的一个好示例。Cooking breakfast is a good example of asynchronous work that isn't parallel. 单人(或单线程)即可处理所有这些任务。One person (or thread) can handle all these tasks. 继续讲解早餐的类比,一个人可以以异步方式做早餐,即在第一个任务完成之前开始进行下一个任务。Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. 不管是否有人在看着,做早餐的过程都在进行。The cooking progresses whether or not someone is watching it. 在开始加热平底锅准备煎蛋的同时就可以开始煎了培根。As soon as you start warming the pan for the eggs, you can begin frying the bacon. 在开始煎培根后,你可以将面包放进烤面包机。Once the bacon starts, you can put the bread into the toaster.

对于并行算法而言,你则需要多名厨师(或线程)。For a parallel algorithm, you'd need multiple cooks (or threads). 一名厨师煎鸡蛋,一名厨师煎培根,依次类推。One would make the eggs, one the bacon, and so on. 每名厨师将仅专注于一项任务。Each one would be focused on just that one task. 每名厨师(或线程)都在同步等待需要翻动培根或面包弹出时都将受到阻。Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

现在,考虑一下编写为 C# 语句的相同指令:Now, consider those same instructions written as C# statements:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

计算机不会按人类的方式来解释这些指令。Computers don't interpret those instructions the same way people do. 计算机将阻塞每条语句,直到工作完成,然后再继续运行下一条语句。The computer will block on each statement until the work is complete before moving on to the next statement. 这将创造出令人不满意的早餐。That creates an unsatisfying breakfast. 后续任务直到早前任务完成后才会启动。The later tasks wouldn't be started until the earlier tasks had completed. 这样做早餐花费的时间要长得多,有些食物在上桌之前就已经凉了。It would take much longer to create the breakfast, and some items would have gotten cold before being served.

如果你希望计算机异步执行上述指令,则必须编写异步代码。If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

这些问题对即将编写的程序而言至关重要。These concerns are important for the programs you write today. 编写客户端程序时,你希望 UI 能够响应用户输入。When you write client programs, you want the UI to be responsive to user input. 从 Web 下载数据时,你的应用程序不应让手机出现卡顿。Your application shouldn't make a phone appear frozen while it's downloading data from the web. 编写服务器程序时,你不希望线程受到阻塞。When you write server programs, you don't want threads blocked. 这些线程可以用于处理其他请求。Those threads could be serving other requests. 存在异步替代项的情况下使用同步代码会增加你进行扩展的成本。Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. 你需要为这些受阻线程付费。You pay for those blocked threads.

成功的现代应用程序需要异步代码。Successful modern applications require asynchronous code. 在没有语言支持的情况下,编写异步代码需要回调、完成事件,或其他掩盖代码原始意图的方法。Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. 同步代码的优点在于易于理解。The advantage of the synchronous code is that it's easy to understand. 分布操作使其易于查看和理解。The step-by-step actions make it easy to scan and understand. 传统的异步模型迫使你侧重于代码的异步性质,而不是代码的基本操作。Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.

不要阻塞,而要 awaitDon't block, await instead

上述代码演示了不正确的实践:构造同步代码来执行异步操作。The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. 顾名思义,此代码将阻止执行这段代码的线程执行任何其他操作。As written, this code blocks the thread executing it from doing any other work. 在任何任务进行过程中,此代码也不会被中断。It won't be interrupted while any of the tasks are in progress. 就如同你将面包放进烤面包机后盯着此烤面包机一样。It would be as though you stared at the toaster after putting the bread in. 你会无视任何跟你说话的人,直到面包弹出。You'd ignore anyone talking to you until the toast popped.

我们首先更新此代码,使线程在任务运行时不会阻塞。Let's start by updating this code so that the thread doesn't block while tasks are running. await 关键字提供了一种非阻塞方式来启动任务,然后在此任务完成时继续执行。The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. “做早餐”代码的简单异步版本类似于以下片段:A simple asynchronous version of the make a breakfast code would look like the following snippet:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

在煎鸡蛋或培根时,此代码不会阻塞。This code doesn't block while the eggs or the bacon are cooking. 不过,此代码也不会启动任何其他任务。This code won't start any other tasks though. 你还是会将面包放进烤面包机里,然后盯着烤面包机直到面包弹出。You'd still put the toast in the toaster and stare at it until it pops. 但至少,你会回应任何想引起你注意的人。But at least, you'd respond to anyone that wanted your attention. 在接受了多份订单的一家餐馆里,厨师可能会在做第一份早餐的同时开始制作另一份早餐。In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

现在,在等待任何尚未完成的已启动任务时,处理早餐的线程将不会被阻塞。Now, the thread working on the breakfast isn't blocked while awaiting any started task that hasn't yet finished. 对于某些应用程序而言,此更改是必需的。For some applications, this change is all that's needed. 仅凭借此更改,GUI 应用程序仍然会响应用户。A GUI application still responds to the user with just this change. 然而,对于此方案而言,你需要更多的内容。However, for this scenario, you want more. 你不希望每个组件任务都按顺序执行。You don't want each of the component tasks to be executed sequentially. 最好首先启动每个组件任务,然后再等待之前任务的完成。It's better to start each of the component tasks before awaiting the previous task's completion.

同时启动任务Start tasks concurrently

在许多方案中,你希望立即启动若干独立的任务。In many scenarios, you want to start several independent tasks immediately. 然后,在每个任务完成时,你可以继续进行已准备的其他工作。Then, as each task finishes, you can continue other work that's ready. 在早餐类比中,这就是更快完成做早餐的方法。In the breakfast analogy, that's how you get breakfast done more quickly. 你也几乎将在同一时间完成所有工作。You also get everything done close to the same time. 你将吃到一顿热气腾腾的早餐。You'll get a hot breakfast.

System.Threading.Tasks.Task 和相关类型是可以用于推断正在进行中的任务的类。The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. 这使你能够编写更类似于实际做早餐方式的代码。That enables you to write code that more closely resembles the way you'd actually create breakfast. 你可以同时开始煎鸡蛋、培根和烤面包。You'd start cooking the eggs, bacon, and toast at the same time. 由于每个任务都需要操作,所以你会将注意力转移到那个任务上,进行下一个操作,然后等待其他需要你注意的事情。As each requires action, you'd turn your attention to that task, take care of the next action, then await for something else that requires your attention.

启动一项任务并等待表示运行的 Task 对象。You start a task and hold on to the Task object that represents the work. 你将首先 await 每项任务,然后再处理它的结果。You'll await each task before working with its result.

让我们对早餐代码进行这些更改。Let's make these changes to the breakfast code. 第一步是存储任务以便在这些任务启动时进行操作,而不是等待:The first step is to store the tasks for operations when they start, rather than awaiting them:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBacon(3);
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Console.WriteLine("Breakfast is ready!");

接下来,可以在提供早餐之前将用于处理培根和鸡蛋的 await 语句移动到此方法的末尾:Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Task<Bacon> baconTask = FryBacon(3);
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");

Console.WriteLine("Breakfast is ready!");

上述代码效果更好。The preceding code works better. 你可以一次启动所有的异步任务。You start all the asynchronous tasks at once. 你仅在需要结果时才会等待每项任务。You await each task only when you need the results. 上述代码可能类似于 Web 应用程序中请求各种微服务,然后将结果合并到单个页面中的代码。The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. 你将立即发出所有请求,然后 await 所有这些任务并组成 Web 页面。You'll make all the requests immediately, then await all those tasks and compose the web page.

与任务组合Composition with tasks

除了吐司外,你准备好了做早餐的所有材料。You have everything ready for breakfast at the same time except the toast. 吐司制作由异步操作(烤面包)和同步操作(添加黄油和果酱)组成。Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). 更新此代码说明了一个重要的概念:Updating this code illustrates an important concept:

重要

异步操作后跟同步操作的这种组合是一个异步操作。The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. 换言之,如果操作的任何部分是异步的,整个操作就是异步的。Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

上述代码展示了可以使用 TaskTask<TResult> 对象来保存运行中的任务。The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks. 你首先需要 await 每项任务,然后再使用它的结果。You await each task before using its result. 下一步是创建表示其他工作组合的方式。The next step is to create methods that represent the combination of other work. 在提供早餐之前,你希望等待表示先烤面包再添加黄油和果酱的任务完成。Before serving breakfast, you want to await the task that represents toasting the bread before adding butter and jam. 你可以使用以下代码表示此工作:You can represent that work with the following code:

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

上述方式的签名中具有 async 修饰符。The preceding method has the async modifier in its signature. 它会向编译器发出信号,说明此方法包含 await 语句;也包含异步操作。That signals to the compiler that this method contains an await statement; it contains asynchronous operations. 此方法表示先烤面包,然后再添加黄油和果酱的任务。This method represents the task that toasts the bread, then adds butter and jam. 此方法返回表示这三个操作的组合的 Task<TResult>This method returns a Task<TResult> that represents the composition of those three operations. 主要代码块现在变成了:The main block of code now becomes:

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 eggs = await eggsTask;
    Console.WriteLine("eggs are ready");
    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");
    var toast = await toastTask;
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");

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

上述更改说明了使用异步代码的一项重要技术。The previous change illustrated an important technique for working with asynchronous code. 你可以通过将操作分离到一个返回任务的新方法中来组合任务。You compose tasks by separating the operations into a new method that returns a task. 可以选择等待此任务的时间。You can choose when to await that task. 可以同时启动其他任务。You can start other tasks concurrently.

高效地等待任务Await tasks efficiently

可以通过使用 Task 类的方法改进上述代码末尾的一系列 await 语句。The series of await statements at the end of the preceding code can be improved by using methods of the Task class. 其中一个 API 是 WhenAll,它将返回一个其参数列表中的所有任务都已完成时才完成的 Task,如以下代码中所示:One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:

await Task.WhenAll(eggTask, 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>Another option is to use WhenAny, which returns a Task<Task> that completes when any of its arguments completes. 你可以等待返回的任务,了解它已经完成了。You can await the returned task, knowing that it has already finished. 以下代码展示了可以如何使用 WhenAny 等待第一个任务完成,然后再处理其结果。The following code shows how you could use WhenAny to await the first task to finish and then process its result. 处理已完成任务的结果之后,可以从传递给 WhenAny 的任务列表中删除此已完成的任务。After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny.

var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
while (allTasks.Any())
{
    Task finished = await Task.WhenAny(allTasks);
    if (finished == eggsTask)
    {
        Console.WriteLine("eggs are ready");
    }
    else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
    }
    else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
    }
    allTasks.Remove(finished);
}
Console.WriteLine("Breakfast is ready!");

进行所有这些更改之后,Main 的最终版本类似于以下代码:After all those changes, the final version of Main looks like the following code:

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 allTasks = new List<Task>{eggsTask, baconTask, toastTask};
    while (allTasks.Any())
    {
        Task finished = await Task.WhenAny(allTasks);
        if (finished == eggsTask)
        {
            Console.WriteLine("eggs are ready");
        }
        else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
        }
        else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
        }
        allTasks.Remove(finished);
    }
    Console.WriteLine("Breakfast is ready!");

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

此最终代码是异步的。This final code is asynchronous. 它更为准确地反映了一个人做早餐的流程。It more accurately reflects how a person would cook a breakfast. 将上述代码与本文中的第一个代码示例进行比较。Compare the preceding code with the first code sample in this article. 阅读代码时,核心操作仍然很明确。The core actions are still clear from reading the code. 你可以按照阅读本文开始时早餐制作说明的相同方式阅读此代码。You can read this code the same way you'd read those instructions for making a breakfast at the beginning of this article. asyncawait 的语言功能支持每个人做出转变以遵循这些书面指示:尽可能启动任务,不要在等待任务完成时造成阻塞。The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don't block waiting for tasks to complete.