.NET 异步,你也许不知道的5种用法
共 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实战项目之CMS 第一章 入门篇-开篇及总体规划
【.NET Core微服务实战-统一身份认证】开篇及目录索引
Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)
.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了
用abp vNext快速开发Quartz.NET定时任务管理界面