c#中使用 async 和 await 的异步编程

发布时间 2023-05-08 09:38:01作者: AI大胜

什么是异步编程

异步编程是对线程的一种应用方式。类似于人跑步时戴着耳机听歌,这两个行为可以同时进行,而不是先跑完步再听歌。异步编程就是同一时间做多件事,通常异步编程就是在继续运行原有逻辑的同时,把耗时的操作放进一个单独的线程中进行并行处理,以重复利用CPU资源以及节省总的运行时间提高效率。

异步达到的效果之一,就比如说避免在进行耗时操作时让用户看到程序“卡死”的现象。

一个图帮助理解异步的意义(详见文档最后:一个官方示例部分):

when any 异步早餐

异步编程模式:

早期分别为.NET 1.0中的APM和.NET 2.0中的EAP。虽然这两种异步编程模式可以实现多数情况下的异步编程,但是它们在MSDN文档上都被标注为了不推荐使用的实现方式(追求编程的简单和程序的高效)

在.NET 4.0中,微软又提供了更简单的异步编程实现方式——TAP,即基于任务的异步模式。

在.NET 4.5中,微软又提出了async和await 两个关键字来支持异步编程。

同步和异步

同步就是程序在一段时间内只做一件事,而异步的作用就是让程序能在同一时间内做多件事。即同步串联,异步并联。

异步方法在完成其工作之前就返回到被调用的那行代码,然后在那行代码的后续代码继续执行的时候完成其工作。

同步方法的特征是在方法A内部,如果调用另外一个方法B,则会跳出当前方法A,去执行方法B,且在方法B执行完所有处理前,不会回到A之前的地方(A中调用B的那一行)。

相反,异步的方法在外部方法处理完成之前就返回到调用方法即调用异步方法的代码处,然后在那行代码的后续代码继续执行的时候完成其工作。

所以呢,同步方法造成的不太好的一点就是:方法A中调用B方法这一行代码处,如果B没执行完成,程序就得等着,等B完成后再执行A中的下一行代码,而在这个等待的时间中,相对于当前程序(A所在的线程)来说,CPU资源就被浪费了,闲着了,什么也没做。

C#中,异步编程要清楚一个核心,两个关键字。一个核心是指TaskTask<T>对象,而两个关键字,就是async和await。

异步方法的运行机制:

img

如果想让一个方法中,人为地让程序暂停几秒钟,可以用:Task.Delay(3000).Wait();

最佳实践: 你可以一次启动所有的异步任务,异步任务即调用异步方法获取task对象, 你仅在需要结果时才会等待(await)每项任务(task对象)。

定义异步方法

c#中,可以用async和await关键字来实现异步,注意这是在c#5.0(.Net Framework 4.5)中才有的。

异步方法的定义和普通方法定义一样,但是:

  1. 方法返回类型必须是:void,Task,Task<T>三者之一。(Task<T>继承自Task类)

  2. 且返回类型左边使用async修饰符。

  3. 方法内部必须有await 表达式,即:await 任务对象

    任务对象的获得有两种方法:

    • 通过调用别的方法,这些方法返回Task类型对象。

    • 用 Task.Run ( ) 的重载方法来自定义。//重要一点:它是在不同的线程上运行你的方法(Task.Run用的是线程池线程)。

Task 和 Task<T>

任务是用于实现称之为并发 Promise 模型的构造。 简单地说,它们“承诺”,会在稍后完成工作.

C#里,Task不是专为异步准备的,它表达的是一个工作在线程池里的一个线程。异步是线程的一种应用,多线程也是线程的一种应用。

Task.Run的内部代码会占用线程池资源,并在一个可用的线程上与主线程并行运行。

异步方法通常返回TaskTask<TResult>

如果方法包含指定TResult类型操作数的return语句,则应该将Task<TResult>作为返回类型。

如果方法不包含任何return语句或包含不返回操作数的return语句,则将Task用作返回类型。

内存泄漏

内存泄漏是指程序在运行过程中,动态分配的内存没有被及时释放,导致系统中可用内存不断减少的现象。

任何使用匿名方法的地方都要避免捕获类的成员,小心内存泄漏。比如在

Task.Run(()=>{……});

这里面,不要用类的字段,要使用的话,尽量先在之前定义个中间变量,让其等于类的某个成员,然后run方法中使用该中间变量。

Task对象的几个方法:

  • Wait:针对单个Task的实例,可以task1.wait进行线程等待
  • WaitAny:线程列表中任何一个线程执行完毕即可执行(阻塞主线程)
  • WaitAll:线程列表中所有线程执行完毕方可执行(阻塞主线程)
  • WhenAny:与ContinueWith配合,线程列表中任何一个执行完毕,则继续ContinueWith中的任务(开启新线程,不阻塞主线程)
  • WhenAll:与ContinueWith配合,线程列表中所有线程执行完毕,则继续ContinueWith中的任务(开启新线程,不阻塞主线程)
  • ContinueWith:与WhenAny或WhenAll配合使用
  • ContinueWhenAny:等价于Task的WhenAny+ContinueWith
  • ContinueWhenAll:等价于Task的WhenAll+ContinueWith

WhenAny和WhenAll

前者是对一组任务进行等待,在有任意一个任务完成时返回一个已完成的Task对象。后者所有任务都完成时返回。

Task<int> finishedTask = await Task.WhenAny(downloadTasks);

async 和 await

await 确实有等待的含义。等什么?等异步操作的运行结果,即等待异步操作执行完毕。

官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/await

awit表达式指定了一个异步执行的任务。其语法为:await task 。由await关键字和一个空闲对象 (称为任务)组成。这个任务可能是一个Task类型的对象,也可能不是。默认情况下,这个任务在当前线程异步运行。

一个空闲对象即是一个awaitable类型的实例。awaitable类型是指包含GetAwaiter方法的类型,该方法没有参数,返回一个称为awaiter类型的对象。awaiter类型包含以下成员:

  • bool IsCompleted
  • void OnCompleted ( Action ) ;

它还包含以下成员之一:

  • void GetResult();
  • T GetResult();(T为任意类型)

然而实际上,你并不需要构建自己的awaitable。相反,你应该使用Task类,它是awaitable 类型。对于awaitable,大多数程序员所需要的就是Task了。 在NET4.5中,微软发布了大量新的和修订的异步方法(在BCL中),它们可返回Task<T>类型的对象。将这些放到你的await表达式中,它们将在当前线程中异步执行。

async 这个关键字在c#中,用于修饰方法,表示该方法是想要定义成异步方法,如果该方法里面有通过await关键字修饰别的方法调用,则定义的方法是异步执行的。

注意1:在异步编程的规范中,async修饰的方法,仅仅表示这个方法在内部有可能采用异步的方式执行,CPU在执行这个方法时,会放到一个新的线程中执行。那这个方法,最终是否采用异步执行,不决定于是否用await 关键字去调用这个方法,而决定于这个方法内部,是否有await方式的调用,有await调用,所以这个方法不管你以什么方式调用,被调用时有没有用await,它都是异步执行的。

注意2:我们需要使用异步的func1方法的返回值。我们可以提前去执行这个方法,而不急于拿到方法的返回值,直到我们需要使用时,再用await去获取到这个返回值去使用。这才是异步对于我们真正的用处。对于一些耗时的IO或类似的操作,我们可以提前调用,让程序可以利用执行过程中的空闲时间来完成这个操作。等到我们需要这个操作的结果用于后续的执行时,我们await这个结果。这时候,如果await的方法已经执行完成,那我们可以马上得到结果;如果没有完成,则程序将继续执行这个方法直到得到结果。

更多请参考:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/

异步方法中捕获异常

若要捕获异步任务引发的异常,将 await 表达式置于 try 块中,并在 catch 块中捕获该异常。

另外注意:返回 void 的异步方法的调用方无法捕获从该方法引发的异常,且此类未经处理的异常可能会导致应用程序故障。 因此如果异步方法的返回值是void,则里面最好用try catch(个人想法)。

取消操作

异步针对的是需要消耗长时间运行的工作。在工作过程中,如果需要,我们可以取消异步的运行。系统提供了一个类CancellationToken来处理这个工作。

CancellationTokenSource 用于向 CancellationToken 发出请求取消的信号。

定义方式:

Task<T> ActionAsync(para ..., CancellationToken cancellationtoken){//代码逻辑……};

调用方式:

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken cancel_token = source.Token;

await ActionAsync(para, cancel_token);

需要取消时,调用Cancel方法就可以了:

source.Cancel();

一段时间后取消

如果想在一段时间后取消异步任务,或者想对在一段时间后还没完成的任务进行取消:

调用:CancellationTokenSource.CancelAfter(Int32)

        try
        {
            source.CancelAfter(3500);

            await ActionAsync(para, cancel_token);
        }
        catch (TaskCanceledException)
        {
            //如果指定时间内await的任务没有完成,就会引发该异常,如果指定时间内完成,则程序将正常完成。
            Console.WriteLine("\nTasks cancelled: timed out.\n");
        }
        finally
        {
            source.Dispose();
        }

一个官方示例

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

when any 异步早餐

参考文档

疑问

  1. 关于线程池中的线程,有个使用建议是:“耗时长或有阻塞情况的不用线程池中的线程”,而定义异步方法的时候,方法内部获取task对象的方式之一是使用 Task.Run() 方法,而Task.Run()使用的是线程池线程,这有点矛盾了,因为本文开头提到:“通常异步编程就是在继续运行原有逻辑的同时,把耗时的操作放进一个单独的线程中进行并行处理,以重复利用CPU资源以及节省总的运行时间提高效率”。这不就是利用线程池线程做耗时长的操作了吗?

    ??


更新于:2023-05-08