首页 > 编程语言 >c#中使用 async 和 await 的异步编程

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

时间:2023-05-08 09:46:17浏览次数:47  
标签:异步 Task Console c# await 线程 async 方法

什么是异步编程

异步编程是对线程的一种应用方式。类似于人跑步时戴着耳机听歌,这两个行为可以同时进行,而不是先跑完步再听歌。异步编程就是同一时间做多件事,通常异步编程就是在继续运行原有逻辑的同时,把耗时的操作放进一个单独的线程中进行并行处理,以重复利用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

标签:异步,Task,Console,c#,await,线程,async,方法
From: https://www.cnblogs.com/idasheng/p/17380761.html

相关文章

  • Node.js躬行记(28)——Cypress自动化测试实践
    最近在研究如何提升项目质量,提炼了许多个用于自测的测试用例,但是每次修改后,都手工测试,成本太高,于是就想到了自动化测试。在一年前已将Cypress集成到管理后台的项目中,不过没有投入到实践中。今天在实践时发现,版本已经到了12.X,当时集成的版本是8.X。一、准备在......
  • 双端VSC-HVDC直流输电仿真模型
    双端VSC-HVDC直流输电仿真模型matlab2021a,采用双环控制,电压环和电流环,电压环采用直流电压参与PI调节,电流内环包括PI调节器与前馈解耦,整流侧与逆变侧为VSC两电平器件。直流波形输出效果理想,交流侧电压电流均为对称的三相电压电流,ID:2545684639072616......
  • 500kV LCC-HVDC直流输电仿真模型Matlab
    500kVLCC-HVDC直流输电仿真模型Matlab采用十二脉波晶闸管换流阀,直流电流为2500A,整流侧采用直流电流PI控制,逆变侧采用直流电压PI控制,可以得到较好的2500A直流电流波形,直流电压在500kV动态平衡,可以得到交流侧多脉波波形,波形质量较好。ID:9640685196769041......
  • [20230508]crack oracle执行文件.txt
    [20230508]crackoracle执行文件.txt--//昨天看了链接:https://www.xifenfei.com/2023/04/ora-07445-kglsget.html--//提到open阶段执行如下:-----CurrentSQLStatementforthissession(sql_id=gtf6tgc2ycgxx)-----selectcount(*)fromXDB.XDB$SCHEMAswheres.xmldata.s......
  • 三电平NPC逆变器矢量控制(SVPWM)matlab2021a
    三电平NPC逆变器矢量控制(SVPWM)matlab2021a采用矢量控制,大扇区、小扇区、矢量作用时间等均用程序编写,可以得到马鞍波调制波形逆变器输出三电平相电压波形,五电平线电压波形,经过滤波器后,可以得到对称的三相电压,电流ID:7440684413273739......
  • ABC020D LCM Rush
    题意:给定\(n,k\le10^9\),求\(\sum\limits_{i=1}^n\operatorname{lcm}(i,k)\bmod(10^9+7)\)的值。定义\(f(x,y)=\sum\limits_{i=1}^x[\gcd(i,y)=1]i\)。容易知道答案\(res=k\sum\limits_{d|k}f(\lfloor\frac{n}{d}\rfloor,\frac{k}{d})\)。转化为求\(f(x,y)......
  • 二极管钳位型NPC逆变器不平衡负载仿真
    二极管钳位型NPC逆变器不平衡负载仿真Matlab2021a采用SPWM调制,双环PI参与控制,逆变器连接LCL滤波器,连接不平衡负载,负载参数可调。可以得到输出线电压为五电平的电压波形,滤波后可以得到对称三相电压波形,电流波形可以根据不平衡负载而发生变化。ID:9925680421021245......
  • 光伏NPC逆变并网仿真matlab2021a
    光伏NPC逆变并网仿真matlab2021a光伏阵列参数已设定,采用mppt算法(扰动观察法);主电路采用二极管钳位型NPC逆变器;采用双闭环控制,电压电流环,PI调节参与;采用正弦脉冲宽度调制;加设LCL滤波器,并入电网。逆变器输出端可以得到五电平线电压波形,滤波后输出对称三相电压波形,稳定后,得到对称三相......
  • ReentrantReadWriteLock源码分析
    ReentrantLock是互斥锁,若存在读多写少同时保证线程安全的场景,ReentrantLock效率比较低,此时需要用到ReentrantReadWriteLock。一、ReentrantReadWriteLock介绍ReentrantReadWriteLock是可重入的读写锁,实现了ReadWriteLock接口,ReadWriteLock是读写锁的顶级接口,定义了readL......
  • ReentrantLock源码分析
    一、ReentrantLock介绍ReentrantLock是JDK1.5引入的,实现Lock接口的互斥锁。保证多线程的环境下,共享资源的原子性。与Synchronized的非公平锁不同,ReentrantLock的实现公平锁、非公平锁。ReentrantLock是重入锁,重入是指,同一个线程可以重复执行加锁的代码段。二、ReentrantLock......