一文看懂"async"和“await”关键词是如何简化了C#中多线程的开发过程

当我们使用需要长时间运行的方法(即,用于读取大文件或从网络下载大量资源)时,在同步的应用程序中,应用程序本身将停止运行,直到活动完成。在这些情况下,异步编程非常有用:它使我们能够并行执行不同任务,并在需要时等待其完成。

这种方法有许多不同的模型类型:APM(异步编程模型),基于事件(异步模型EAP),以及TAP,基于任务的(异步模型任务)。让我们看看如何使用关键字async和await在C#中实现第三个方法。

编写异步代码的主要问题之一是可维护性:实际上,许多人普遍认为这种编程方法会使代码复杂化。幸运的是,C#5引入了一种简化的方法,在该方法中,编译器运行由开发人员先前完成的艰巨任务,并且应用程序保留类似于同步代码的逻辑结构。

让我们举个例子。假设我们有一个.NET Core项目,我们应该在其中管理三个实体:Area,Company和Resource。

public class Area
{public int Id { get; set; }[Required][StringLength(255)]public string Name { get; set; }
}public class Company
{public int Id { get; set; }[Required][StringLength(255)]public string Name { get; set; }
}public class Resource
{public int Id { get; set; }[Required][StringLength(255)]public string Name { get; set; }
}

现在假设我们应该使用Entity Framework Core将这些实体的值保存在数据库中。其DbContext是:

public class AppDbContext : DbContext
{public DbSet<Area> Areas { get; set; }public DbSet<Company> Companies { get; set; }public DbSet<Resource> Resources { get; set; }public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)  {}override protected void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity<Area> ().HasData(new Area { Id = 1, Name = "Area1"},new Area { Id = 2, Name = "Area2"},new Area { Id = 3, Name = "Area3"},new Area { Id = 4, Name = "Area4"},new Area { Id = 5, Name = "Area5"});modelBuilder.Entity<Company> ().HasData(new Area { Id = 1, Name = "Company1"},new Area { Id = 2, Name = "Company2"},new Area { Id = 3, Name = "Company3"},new Area { Id = 4, Name = "Company4"},new Area { Id = 5, Name = "Company5"});modelBuilder.Entity<Resource>().HasData(new Area { Id = 1, Name = "Resource1"},new Area { Id = 2, Name = "Resource2"},new Area { Id = 3, Name = "Resource3"},new Area { Id = 4, Name = "Resource4"},new Area { Id = 5, Name = "Resource5"});}
}

从代码中可以看到,我们插入了一些示例数据进行处理。现在假设我们要使用Controller API公开这些数据,既单独(针对每个实体),又使用将它们全部联接在一起的方法,并通过一次调用返回它们。

使用同步方法,Controller API 将是:

[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{private readonly AppDbContext db = null;public DataController(AppDbContext db){this.db = db;}public IActionResult Get(){var areas = this.GetAreas();var companies = this.GetCompanies();var resources = this.GetResources();return Ok(new { areas = areas, companies = companies, resources = resources });}[Route("areas")]public Area[] GetAreas() {return this.db.Areas.ToArray();}[Route("companies")]public Company[] GetCompanies() {return this.db.Companies.ToArray();}[Route("resources")]public Resource[] GetResources() {return this.db.Resources.ToArray();}
}

Get()方法在其中调用返回单个结果的三个方法,并等待每个方法的执行完成后再传递到下一个结果。这三种方法互不相关,因此您无需等待其中一种方法的执行即可调用另一种方法。然后,您可以创建三个独立的任务以并行执行。
第一种方法可以基于该方法Task.Run()作业运行在线程池之上,并返回一个任务对象,它代表了这项工作。这样,方法可以在线程池的不同线程上同时运行:

public IActionResult Get()
{var areas = Task.Run(() = > this.GetAreas());var companies = Task.Run(() = > this.GetCompanies());var resources = Task.Run(() = > this.GetResources());        Task.WhenAll(areas, companies, resources);return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}

TaskResult属性包含详细说明的结果。方法WhenAll允许暂停当前线程执行,直到所有Task完成。运行代码,我们可以注意到一个有趣的事情:调用中断,并启动以下异常:

AggregateException:发生一个或多个错误。(在上一个操作完成之前,第二个操作在此上下文上开始。这通常是由使用相同DbContext实例的不同线程引起的。有关如何避免DbContext线程问题的更多信息,请参见https://go.microsoft.com/fwlink/?linkid=2097913。[1]

此错误消息告诉我们,方法在不同的线程上同时执行,但是由于它们使用与DbContext 相同的实例来连接数据库, 因此引发了异常,DbContext类无法确保线程安全的功能:我们可以轻松地绕过此问题,避免了.NET Core 的依赖注入引擎创建单个实例,而我们为每种方法创建了单独的实例。作为示例,让我们看看方法GetAreas()会如何变化:

public class DataController : ControllerBase
{private readonly DbContextOptionsBuilder <AppDbContext> optionsBuilder = null;public DataController(IConfiguration configuration){this.optionsBuilder = new DbContextOptionsBuilder <AppDbContext> ().UseSqlite(configuration.GetConnectionString("DefaultConnection"));}[Route("areas")]public Area[] GetAreas() {using(var db = new AppDbContext(this.optionsBuilder.Options)){return db.Areas.ToArray();}}
}

好吧,现在可以了。我们应该注意,EFCore提供了一些方法,例如,与方法ToArrayAsync一样,使用相同的DbContext进行异步调用,该方法从IQueryable 创建一个数组,该数组  异步枚举它。此方法返回Task ,它是表示异步操作的活动。

这样,我们不再需要使用Task.Run():

public IActionResult Get()
{var areas = this.GetAreas();var companies = this.GetCompanies();var resources = this.GetResources();Task.WhenAll(areas, companies, resources);return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}[Route("areas")]
public Task<Area[]> GetAreas()
{return db.Areas.ToArrayAsync();
}

无论如何,Microsoft不能保证这些异步方法在每种情况下都能工作,因为DbContext尚未设计为线程安全的。您可以查询此链接以获取更多信息:https : //docs.microsoft.com/zh-cn/ef/core/querying/async

使用Entity Framework Core时,最佳实践是在启动另一个异步操作之前,为每个异步操作都拥有一个DbContext或等待每个异步操作完成。当我们必须进行异步调用并返回结果时,这种最佳做法是可以的。

但是,如果我们想在返回结果之前对结果进行一些操作,会发生什么?如果我们想向列表中添加元素怎么办?我们应该等待结果,添加元素,然后返回修改后的列表:

[Route("companies")]
public Task<Company[]> GetCompanies()
{using (var db = new AppDbContext(this.optionsBuilder.Options)){var data = this.db.Companies.ToListAsync().Result;data.Insert(0, new Company() { Id = 0, Name = "-"});return data.ToArray();}
}

不幸的是,该代码无法编译,因为data.ToArray()返回的是数组而不是Task。实际上,这里我们需要三个线程:主调用方(Get()),数据库查询(this.db.Companies.ToListAsync())和一个线程,该线程将一个值添加到列表中。我们有三种方法可以做到这一点:让我们用三种单一方法来查看它们。我们已经看到的第一个,可以使用Task.Run()方法:

[Route("companies")]
public Task<Company[]> GetCompanies()
{return Task.Run(() =>{using (var db = new AppDbContext(this.optionsBuilder.Options)){var data = db.Companies.ToList();data.Insert(0, new Company() { Id = 0, Name = "-" });return data.ToArray();}});
}

作为替代方案,我们可以使用方法ContinueWith(),该方法可以应用于任务,并且可以在上一个方法完成后立即指定要运行的新任务:

[Route("resources")]
public Task <Resource[]> GetResources()
{using (var db = new AppDbContext(this.optionsBuilder.Options)){return db.Resources.ToListAsync().ContinueWith(dataTask = >{var data = dataTask.Result;dataTask.Result.Insert(0, new Resource() { Id = 0, Name = "-" });return data.ToArray();});}
}

我们可以让编译器执行“垃圾代码”,并使用关键字asyncawait,这可以为我们创建Task:

[Route("areas")]
public async Task <Area[]> GetAreas()
{using (var db = new AppDbContext(this.optionsBuilder.Options)){var data = await db.Areas.ToListAsync();data.Insert(0, new Area() { Id = 0, Name = "-" });return data.ToArray();}
}

正如您在最后一种方法中看到的那样,代码更加简单,并且向我们隐藏了Task的创建,从而使我们可以异步返回。让我们想象一下一个场景,其中调用不止一个,并且这种方法如何使一切变得更加线性。

重构的作用是方法GetAreas()已成为异步操作。这个事实意味着,当不同的请求到达此API时,分配给该请求的线程池的线程将被释放以供其他请求使用,直到DbContext终止数据提取为止。

我希望我能引起您足够的兴趣来深入分析该论点。在许多情况下,使用async和await非常方便,并且除了使代码更加简洁和线性外,还可以提高一般应用程序的性能。

示例代码见:

https://github.com/fvastarella/Programmazione-asincrona-con-async-await

References

[1] https: https://docs.microsoft.com/en-us/ef/core/querying/async
[2] //docs.microsoft.com/zh-cn/ef/core/querying/async: https://docs.microsoft.com/en-us/ef/core/querying/async

一文看懂async和“await”关键词是如何简化了C#中多线程的开发过程相关推荐

  1. 一文看懂「生成对抗网络 - GAN」基本原理+10种典型算法+13种应用

    生成对抗网络 – Generative Adversarial Networks | GAN 文章目录 GAN的设计初衷 生成对抗网络 GAN 的基本原理 GAN的优缺点 10大典型的GAN算法 GA ...

  2. 「最有用」的特殊大数据:一文看懂文本信息系统的概念框架及功能

    导读:作为一种特殊的大数据,文本数据泛指各种以自然语言形式存在的数据. 目前,我们正处在一个以大数据与人工智能技术为核心的新的工业革命时代,其主要特征是大量各种可利用的数据可以视为一种特殊的生产资料, ...

  3. 一文看懂JUC之AQS机制

     作者:VectorJin juejin.cn/post/6844904041760161806 为了解决原子性的问题,Java加入了锁机制,同时保证了可见性和顺序性.JDK1.5的并发包中新增了Lo ...

  4. 一文看懂-ElasticSearch全文搜索引擎

    一文看懂-ElasticSearch全文搜索引擎 一.ElasticSearch简介 1.1 什么是ElasticSearch ElasticSearch简称ES,其中Elastic 从名字里我们可以 ...

  5. 一文看懂推荐系统:物品冷启02:简单的召回通道

    一文看懂推荐系统:物品冷启02:简单的召回通道 提示:最近系统性地学习推荐系统的课程.我们以小红书的场景为例,讲工业界的推荐系统. 我只讲工业界实际有用的技术.说实话,工业界的技术远远领先学术界,在公 ...

  6. 一文看懂 AI 训练集、验证集、测试集(附:分割方法+交叉验证)

    2019-12-20 20:01:00 数据在人工智能技术里是非常重要的!本篇文章将详细给大家介绍3种数据集:训练集.验证集.测试集. 同时还会介绍如何更合理的讲数据划分为3种数据集.最后给大家介绍一 ...

  7. 一文看懂计算机视觉-CV(基本原理+2大挑战+8大任务+4个应用)

    2020-03-06 20:00:00 计算机视觉(Computer Vision)是人工智能领域的一个重要分支.它的目的是:看懂图片里的内容. 本文将介绍计算机视觉的基本概念.实现原理.8 个任务和 ...

  8. 一文看懂人脸识别(4个特点+4个实现步骤+5个难点+算法发展轨迹)

    2020-03-09 20:01:00 人脸识别是身份识别的一种方式,目的就是要判断图片和视频中人脸的身份时什么. 本文将详细介绍人脸识别的4个特点.4个步骤.5个难点及算法的发展轨迹. 什么是人脸识 ...

  9. 一文看懂卷积神经网络-CNN(基本原理+独特价值+实际应用)

    http://blog.itpub.net/29829936/viewspace-2648775/ 2019-06-25 21:31:18 卷积神经网络 – CNN 最擅长的就是图片的处理.它受到人类 ...

最新文章

  1. Nature子刊:遗传发育所白洋组发表高通量分离培养和鉴定根系细菌的方法
  2. 数据结构利器之私房STL(上)
  3. Python中join 和 split详解
  4. PowerDesigner将PDM导出生成WORD文档
  5. word打开文档很久很慢_word文档打开特别慢怎么解决,word10打开文档很慢
  6. java调用keras theano模型_使用Keras获得模型输出的梯度w.r.t权重
  7. matlab aic sic,sic是什么意思_sic的翻译_音标_读音_用法_例句_爱词霸在线词典
  8. 遍历窗体中所有控件的信息
  9. linux将所有文件生成lst_10行Python代码自动清理电脑内重复文件,解放双手!
  10. makefile 打印变量_通过实例学Makefile
  11. C++Primer第四版 阅读笔记 第二部分 “容器和算法”
  12. Cooliris – 优雅的照片浏览工具[iOS/Android]
  13. Python 语言程序设计(3-1)字符串处理函数和相关功能
  14. Spring定时器技术终结者——采用Scheduled注释的方式实现Spring定时器
  15. Java Agent实战
  16. java实现选择排序(思路与实现)
  17. mysql中可以查询英文却查不了中文或数字
  18. 从零开始的MySQL数据库三部曲(二、MySQL数据库的创库创表增删改查篇)
  19. python实现蜂鸣器演奏两只老虎
  20. Mysql为什么使用B+树(一)之红黑树简述

热门文章

  1. Ubuntu16.04 - 安装RabbitVCS,linux下的TortoiseSVN!!!
  2. 解决java.lang.NoClassDefFoundError: org/aopalliance/intercept/MethodInterceptor问题
  3. webrtc 视频 demo
  4. UILabel的高度自适应
  5. 解题报告 树形图计数
  6. python 新闻摘要_每日新闻摘要:Microsoft内部禁止应用程序,这样就可以了
  7. 计算机启动程序bios_如何构建自己的计算机,第三部分:准备BIOS
  8. Windows端口被占用处理方法
  9. Windows Server 2012活动目录基础配置与应用(新手教程)之3---将客户机加入到指定域...
  10. 在PowerDesigner中设计物理模型1——表和主外键