.NET 异步,你也许不知道的5种用法

DotNetCore实战

共 7233字,需浏览 15分钟

 ·

2021-04-01 17:30


async/await异步操作,是C#中非常惊艳的“语法糖”,让异步编程变得优美且傻瓜化到了不可思议的程度。就连JavaScript都借鉴了async/await语法,让回调泛滥的JavaScript代码变得很优美。

我之前录制的.NET视频教程已经把async/await等基础知识介绍了,这篇文章不再介绍那些基础知识,如果有对它们还不了解的朋友,请到我的B站、头条、油管等平台搜索“杨中科 .net 教程”查看。

本篇文章只对在之前的视频教程中没有提到的几点做讲解。

 

用法1、控制并行执行的任务数量

       在项目开发的时候,有时候有很多任务需要异步执行,但是为了避免同时执行的异步任务太多,反而降低性能,因此通常需要限制并行执行的任务的数量。比如爬虫并行从网上抓取内容的时候,就要根据情况限制最大执行的线程的数量。

在没有async/await的年代,需要使用信号量等机制来进行线程间通讯来协调各个线程的执行,需要开发者对于多线程的技术细节非常了解。而使用async/await之后,这一切就可以变得非常傻瓜化了。

比如下面的代码用来首先从words.txt这个每行一个英文单词的字典中,逐个读取单词,然后调用一个API接口来获得单词的“音标、中文含义、例句”等详细信息。为了加快处理速度,需要采用异步编程来实现多任务同时下载,但是又要限制同时执行的任务的数量(假设为5个)。实现代码如下:

class Program{       static async Task Main(string[] args)       {              ServiceCollectionservices = new ServiceCollection();              services.AddHttpClient();              services.AddScoped<WordProcessor>();              using(var sp = services.BuildServiceProvider())              {                     var wp = sp.GetRequiredService<WordProcessor>();                     string[]words = await File.ReadAllLinesAsync("d:/temp/words.txt");                     List<Task>tasks = new List<Task>();                     foreach(var word in words)                     {                            tasks.Add(wp.ProcessAsync(word));                            if(tasks.Count==5)                            {                                   //waitwhen five tasks are ready                                   awai tTask.WhenAll(tasks);                                   tasks.Clear();                            }                     }                     //waitthe remnant which are less than five.                     await Task.WhenAll(tasks);              }              Console.WriteLine("done!");       }} class WordProcessor{       private IHttpClientFactory httpClientFactory;       public WordProcessor(IHttpClientFactory httpClientFactory)       {              this.httpClientFactory= httpClientFactory;       }        publicasync Task ProcessAsync(string word)       {              Console.WriteLine(word);              var httpClient = this.httpClientFactory.CreateClient();              string json = await httpClient.GetStringAsync("http://dict.e.opac.vip/dict.php?sw="+ Uri.EscapeDataString(word));              await File.WriteAllTextAsync("d:/temp/words/" + word + ".txt",json);       }}


 

核心代码就是下面这一段:

List<Task> tasks = newList<Task>();foreach(var word in words){       tasks.Add(wp.ProcessAsync(word));       if(tasks.Count==5)       {              //waitwhen five tasks are ready              await Task.WhenAll(tasks);              tasks.Clear();       }}


这里遍历所有单词,抓取单词并且保存到磁盘的Process方法的返回值Task没有使用await关键字进行修饰,而是把返回的Task对象保存到list中,由于没有使用await进行等待,因此不用等一个任务执行完成,就可以把下一个任务加入list。当list中的任务满五个的时候,就调用await Task.WhenAll(tasks);等待这五个任务执行完成后,再处理下一组(5个)。循环之外的await Task.WhenAll(tasks);的是用来处理最后一组不足5个任务的情况。

 

用法2、在BackgroundService等异步执行的代码中进行DI注入

 

    依赖注入(DI)的时候,注入的对象都是有生命周期的。比如使用services.AddDbContext<TestDbContext>(...);这种方式注入EF Core中的DbContext的时候,TestDbContext的生命周期就是Scope。在普通的MVC的Controller中可以直接注入TestDbContext,但是在BackgroundService中是不能直接注入TestDbContext的。这时候,可以注入IServiceScopeFactory对象,然后在使用到TestDbContext对象的时候再调用IServiceScopeFactory的CreateScope()方法来生成一个IServiceScope,并且使用IServiceScope的ServiceProvider来手动解析获取TestDbContext对象。

代码如下:

public classTestBgService:BackgroundService{       private readonly IServiceScopeFactory scopeFactory;       public TestBgService(IServiceScopeFactory scopeFactory)       {              this.scopeFactory= scopeFactory;       }        protected override Task ExecuteAsync(CancellationToken stoppingToken)       {              using(var scope = scopeFactory.CreateScope())              {                     var sp = scope.ServiceProvider;                     var dbCtx = sp.GetRequiredService<TestDbContext>();                     foreach(var b in dbCtx.Books)                     {                            Console.WriteLine(b.Title);                     }              }                             return Task.CompletedTask;       }}


 

用法3、异步方法可以不await

我在做youzack背单词的时候,有一个查询单词的功能。为了提升客户端的响应速度,我把每个单词的明细信息都按照“每个单词一个json文件”的形式,把单词的详细信息保存到文件服务器,相当于做了一个“静态化”。因此客户端在查询单词的时候,先到文件服务器中查找一下是否有对应的静态文件,如果有的话,就直接加载静态文件。如果在文件服务器不存在的话,再调用API接口的方法去查询,API接口从数据库中查询到单词后,不仅会把单词的详细信息返回给客户端,而且还会把单词的详细信息再上传到文件服务器。这样以后客户端再查询这个单词,就可以直接从文件服务器查询了。


因此API接口中“把从数据库中查询到的单词的详细信息上传到文件服务器”这个操作对于接口的请求者来讲没什么意义,而且会降低接口的响应速度,因此我就把“上传到文件服务器”这个操作写到了异步方法中,并且没有通过await来等待。

伪代码如下:

public async Task<WordDetail>FindWord(string word){       var detail = await db.FindWordInDBAsync(word);//从数据库里查询       _=storage.UploadAsync($”{word}.json”,detail.ToJsonString());//上传到文件服务器,但是不等待       returnd etail;}


 

在上面的UploadAsync调用中没有await调用等待,因此只要从数据库中查询出来,就把detail返回给请求者了,留下UploadAsync在异步线程中慢慢执行。

 

前面加的“_=”是消除对于不await异步方法造成编译器警告。

 

用法4、异步代码中Sleep的坑

 

    在编写代码的时候,有时候我们需要“暂停一段时间,再继续执行代码”。比如调用一个Http接口,如果调用失败,则需要等待2秒钟再重试。

    在异步方法中,如果需要“暂停一段时间”,那么请使用Task.Delay(),而不是Thread.Sleep(),因为Thread.Sleep()会阻塞主线程,就达不到“使用异步提升系统并发能力”的目的了。

如下代码是错误的:

public async Task<IActionResult> TestSleep(){       await System.IO.File.ReadAllTextAsync("d:/temp/words.txt");       Console.WriteLine("firstdone");       Thread.Sleep(2000);       awaitSystem.IO.File.ReadAllTextAsync("d:/temp/words.txt");       Console.WriteLine("seconddone");       returnContent("xxxxxx");}


上面的代码是能够正确的编译执行的,但是会大大降低系统的并发处理能力。因此要用Task.Delay()代替Thread.Sleep()。如下是正确的:

public async Task<IActionResult> TestSleep(){       awaitSystem.IO.File.ReadAllTextAsync("d:/temp/words.txt");       Console.WriteLine("firstdone");       awaitTask.Delay(2000);//!!!       awaitSystem.IO.File.ReadAllTextAsync("d:/temp/words.txt");       Console.WriteLine("seconddone");       returnContent("xxxxxx");}


 

用法5、yield如何用到异步方法中

    yield由于可以实现“产生一个数据就让IEnumerable的使用者处理一个数据”,从而实现数据处理的“流水线化”,提升数据处理的速度。

    但是,由于yield和async都是编译器提供的语法糖,编译器都会把它们修饰的方法编译为一个使用了状态机的类。因此两个语法糖碰到一起,编译器就迷惑了,因此不能直接在async修饰的异步方法中使用yield返回数据。

因此下面的代码是错误的:

static async IEnumerable<int>ReadCC(){       foreach(string line in await File.ReadAllLinesAsync("d:/temp/words.txt"))       {              yieldreturn line.Length;       }}


只要把IEnumerable改成IAsyncEnumerable就可以了,如下是正确的:

static async IAsyncEnumerable<int>ReadCC(){       foreach(stringline in await File.ReadAllLinesAsync("d:/temp/words.txt"))       {              yieldreturn line.Length;       }}


但是调用同时使用了async和yield的代码,不能使用普通的foreach+await,如下是错误的:

foreach (int i in await ReadCC()){       Console.WriteLine(i);}


 

需要把await关键词移动到在foreach之前,如下是正确的:

await foreach(int i in ReadCC()){       Console.WriteLine(i);}


编译器是微软写的,不知道为什么不支持foreach (int i in awaitReadCC())这样的写法,可能是由于为了兼容之前的C#语法规范不得已而为之吧。


往期精彩回顾




【推荐】.NET Core开发实战视频课程 ★★★

.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划

【.NET Core微服务实战-统一身份认证】开篇及目录索引

Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

10个小技巧助您写出高性能的ASP.NET Core代码

用abp vNext快速开发Quartz.NET定时任务管理界面

在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度

现身说法:实际业务出发分析百亿数据量下的多表查询优化

关于C#异步编程你应该了解的几点建议

C#异步编程看这篇就够了


浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报