0
点赞
收藏
分享

微信扫一扫

Asynchronous programming with async and await

以前干嘛去了 2022-02-17 阅读 31
c#.net

Official documentation link

Asynchronous programming with async and await

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. The compiler performs many 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:(这就是这种语法的目标,this译成这) 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. 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(would:可以) 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(烹饪pengren), 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(v 进行) 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(弹出).

Now, consider those same instructions written as C# statements:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    class Program
    {
        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!");
        }

        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 Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

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

            return new Bacon();
        }

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

            return new Egg();
        }

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

The synchronously prepared breakfast, took roughly(大致,大约) 30 minutes because the total is the sum of each individual task.

Note:

The CoffeeEggBaconToast, and Juice classes are empty. They are simply marker classes(标记类) for the purpose of demonstration, contain no properties(不包含任何属性), and serve no other purpose(也没有任何其他用途).

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. When you write client programs, you want the UI to be responsive to user input. 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 its 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.

Don'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. 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 FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    Bacon bacon = await FryBaconAsync(3);
    Console.WriteLine("bacon is ready");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

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

Important:

The total elapsed time(运行时间) is roughly(大致) the same as the initial(初始) synchronous version. The code has yet to(还没有) take advantage of(利用) some of the key features of asynchronous programming.

Tip:

The method bodies of the FryEggsAsyncFryBaconAsync, and ToastBreadAsync have all been updated to return Task<Egg>Task<Bacon>, and Task<Toast> respectively(分别). The methods are renamed from their original version to include the "Async" suffix(这些方法从其原始版本被重新命名为包含Async后缀). Their implementations are shown as part of the final version later in this article(他们的实现将在本文后面的最终版本中显示).

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(这种改变就是我们所需要的). 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.

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.

You start a task and hold on to(持有) the Task object that represents the work. 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> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

Task<Toast> toastTask = ToastBreadAsync(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!");

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> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(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 eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

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

The asynchronously prepared breakfast took roughly 20 minutes, this time savings(节省时间) is because some tasks ran concurrently.

The preceding code works better(上述代码效果更好). You start all the asynchronous tasks at once. You await each task only when you need the results. 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. 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:

Important:

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.

The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks(保存正在运行的任务). 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:

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

    return toast;
}

The preceding method has the async modifier(修饰符) in its signature(签名). 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. 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!");
}

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.

Asynchronous exceptions

Up to this point(到目前为止), you've implicitly assumed that(您已隐含的假设) all these tasks complete successfully. Asynchronous methods throw exceptions, just like their synchronous counterparts(对应物). Asynchronous support for exceptions and error handling strives for(力求) the same goals as asynchronous support in general(异常和错误处理的异步支持力求和一般异步支持目标相同): You should write code that reads like a series of synchronous statements. Tasks throw exceptions when they can't complete successfully. The client code can catch those exceptions when a started task is awaited. For example, let's assume that the toaster catches fire(着火了) while making the toast. You can simulate(模拟) that by modifying the ToastBreadAsync method to match the following code:

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(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

Note:

You'll get a warning when you compile the preceding code regarding(关于) unreachable(不可访问) code. That's intentional(故意的), because once the toaster catches fire, operations won't proceed normally.

Run the application after making these changes, and you'll output similar to the following text:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   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)

Notice that there's quite a few(很多) tasks completing between when the toaster catches fire and the exception is observed(在面包机着火和观察到异常期间). When a task that runs asynchronously throws an exception, that Task is faulted. The Task object holds(保存) the exception thrown in the Task.Exception property. Faulted tasks throw an exception when they're awaited.

There are two important mechanisms(机制) to understand: how an exception is stored in a faulted task, and how an exception is unpackaged(解包) and rethrown(重新抛出) when code awaits a faulted task.

When code running asynchronously throws an exception, that exception is stored in the Task. The Task.Exception property is an System.AggregateException because more than one exception may be thrown during asynchronous work. Any exception thrown is added to the AggregateException.InnerExceptions collection. If that Exception property is null, a new AggregateException is created and the thrown exception is the first item in the collection.

The most common scenario for a faulted task is that the Exception property contains exactly one exception. When code awaits a faulted task, the first exception in the AggregateException.InnerExceptions collection is rethrown. That's why the output from this example shows an InvalidOperationException instead of an AggregateException. Extracting(提取) the first inner exception makes(使得) working with(使用) asynchronous methods as similar as possible to(尽量类似于) working with their synchronous counterparts(使用他们的同步方法). You can examine(检查) the Exception property in your code when your scenario(方案) may generate multiple exceptions.

Before going on, comment out(注释掉) these two lines in your ToastBreadAsync method. You don't want to start another fire:

Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");

Await tasks efficiently

The series of await statements at the end of the preceding code can be improved by using methods of the Task class. 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(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

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. The following code shows how you could use WhenAny to await the first task to finish and then process(处理) its result. After processing the result from the completed task, you remove that completed task from the list of tasks passed to 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");
    }
    breakfastTasks.Remove(finishedTask);
}

After all those changes, the final version of the code looks like this:

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

namespace AsyncBreakfast
{
    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");
                }
                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();
        }
    }
}

The final version of the asynchronously prepared breakfast took roughly 15 minutes because some tasks ran concurrently(同时), and the code monitored multiple tasks at once(并且代码一次监控多个任务) and only took action when it was needed.

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. 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.

举报

相关推荐

0 条评论