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

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#语法规范不得已而为之吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值