1. 为什么会有/怎么解决: async/await的无限嵌套

public async Task<int> myFuncAsync1()
{
//some code
  int num = await getNumberFromDatabaseAsync(); //如果没有await那么async修饰的函数仍然是同步执行,失去意义
  return num;
} public async Task<string> myFuncAsync2()
{ 
  //some code
  int num = await myFuncAsync1(); //因为用await等待async函数,所以此函数也要标记为async
  string s = ""+ num.toString();
  return s;
}
public async Task<int> myFuncAsync3()
{
...
}
...

第一次遇到async/await是在做一个智能家居的网络控制程序上,为了不阻塞UI,老同事说把其中有的方法改成了async,让后端修改数据库的逻辑异步执行,返回操作结果之后再刷新UI,可是发现把一个函数标记成async之后,你就必须在这个函数里头有await的对象(通常也是一个异步函数)才能让async真的异步,否则即使标记async函数仍然会同步执行,那这个await的对象又一定要是一个async的函数,同理,刚才说的函数的母函数里头,他又需要在一个await修饰调用的这个函数才能等到结果,有了await母函数也要标记为async,那是不是无穷无尽了?

答案是否定的。

先说下为什么为引起这样的原因。正如上文提到的,一般是因为你想异步执行什么操作,归根结底就是异步的数据库操作或者调用异步API,比如调用 QueryAsync()或者httpGetAsync(),这些ORM或者API已经被分装成async的形式了,你为了等他们结束获取结果,需要await他们,而await关键字只能在async函数中使用,以此类推。。所以如果你看到一个async函数,一直向下查看的话,应该能看到最底层的应该是我刚才说的调用的别人封装好的异步函数。比如下图这个是dapper(一个轻量级ORM)对于执行一些数据库query的异步方法,为了调用他们可能会发生上述情况。

那怎么解决这个无限的循环呢?因为大家都知道一直往上肯定是到main函数了,main函数是不能被标记为async的,总得有一个async函数被包含在没有async标记的函数里。当然这里有一种情况就是最底层的async函数层层异步到最上端,作为API被暴露给外界,这也就是我们调用的异步API同理,适用于REST API的那种项目。那么如果不是从顶层async到底层,像一个UI函数,怎么调用一个异步函数呢?

有两种方法:

public static void main()
{
//some code
  myFuncAsync1();
} public async Task myFuncAsync1()
{ 
  //some code
  string s= await myFuncAsync2();
  someLogic(s);
}
public async Task<string> myFuncAsync2()
{
//some code
  int num = await myFuncAsync3();
  string s = someLogic(num);
  return s;
}

第一个是封装到一个没有返回值的异步函数里头, 比如我们的逻辑从底层的myFuncAsyncN一直返回值到myFuncAsync1,在myFuncAsync1中,拿到myFuncAsync2的数据并且完成最后所有操作,不需要再返回任何值进行进一步的操作,那么myFuncAsync1虽然被标记为async函数,但是在main函数调用他的时候,因为没有返回值,所以不需要用await关键字。事实上async/await的循环链都可以止步于一个没有返回值的async函数。

public String DownloadString(String url)
{
  var request = HttpClient.GetAsync(url).Result;
  var download = request.Content.ReadAsStringAsync().Result;
  return download;
}

第二个是利用Task.Result来直接获取结果(会阻塞主进程,慎用!)。如上段代码,本来的async函数,需要用await修饰来异步获取结果,然后再继续执行,现在用result直接等到当前进程把这个异步函数运行完并且拿到结果,result关键字其实是执行完这个task并且拿到结果,否则针对task返回型我们只能await修饰来等待结果。这样做的话避免了await,母函数也就不用async修饰,中断了async/await链。但是用result关键字,其实是阻塞的主进程,然后在一个新进程上运行异步task,得到结果之后,返回给主进程,主进程再继续往下走,注意主进程在新进程去获取结果的这段时间,是不能做别的事的,只能干等在这里,所以和主进程自己去做这个task然后得到结果并没有卵区别,可以说就是一个多浪费了一个进程的同步。而await修饰的话,主进程到了await这里就可以被释放干别的去,如果主进程是UI进程的话,UI就不会卡顿。事实上await保存了上下文,封装了后半部分代码,等到await等来了结果,他会安排一个新的进程,给他上下文让他继续运行,所以await才真正做到了充分利用进程。

public String DownloadString(String url)
{
  var runInBackground = Task.Run(()=>HttpClient.GetAsync(url)); //假设GetAsync耗时5秒
  var runInBackground2 = Task.Run(()=>sql.QueryAsync(q)) //也耗时5秒
  //上边两个task并行
  var request = runInBackground.Result; //5s之后拿到结果,主进程阻塞了5秒
  var db = runInBackground.Result; //同时拿到结果,无需等待
  //两个task共耗时5秒
  ...
}

那有人就问了,既然这样,result关键字好像有百害而无一利,为什么还要用,其实我们可以配合Task.Run()使用,Task.Run()会在后台开一个新进程1去运行GetAsync这个task需要5秒,同时因为没有使用result关键字,主进程并没有马上需要用到result,会继续往下执行,假设下边有另外一个Task.Run()开启新进程2去执行另外一个task,也耗时5秒,然后到获取第一个result时候,主进程阻塞5秒,新进程1返回结果,在新进程1获取结果这5秒,新进程2也同时拿到了结果,所以第二个result主进程不会阻塞,直接拿到了结果,这也是完成了两个并行的异步任务。和await相比主进程只是在被阻塞的5秒内不能做别的事。如果是UI进程的话,相对于上段代码的阻塞10秒,这里就只阻塞5秒。






小白终于弄懂了:c#从async/await到Task再到Thread的更多相关文章

  1. 我终于弄懂了Python的装饰器(一)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 一 ...

  2. 我终于弄懂了Python的装饰器(二)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 二 ...

  3. 我终于弄懂了Python的装饰器(四)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 四 ...

  4. async,await与task.wait()或task.Result的区别

    你是否曾经与我一样不理解async,await与task.wait()或者task.Result的区别? 接下来,一个Demo让你看出他们之间的区别. static void Main(string[ ...

  5. 初步学习async/await,Task.GetAwaiter,Task.Result

    网上关于async/await的知识有很多,看了很多但不如自己实践一遍来得快,所以这里记录下我的理解和大家学习下. 首先以最简单的同步方法来开始如下 private static void Test( ...

  6. [转]Hibernate与Jpa的关系,终于弄懂

    原文地址:http://blog.sina.com.cn/s/blog_5f1619e80100yoxz.html 我知道Jpa是一种规范,而Hibernate是它的一种实现.除了Hibernate, ...

  7. Hibernate与Jpa的关系,终于弄懂

    我知道Jpa是一种规范,而Hibernate是它的一种实现.除了Hibernate,还有EclipseLink(曾经的toplink),OpenJPA等可供选择,所以使用Jpa的一个好处是,可以更换实 ...

  8. 移动设备分辨率(终于弄懂了为什么移动端设计稿总是640px和750px)

    在我开始写移动端页面至今,一直有2个疑问困扰着我,我只知道结果但不知道为什么 问题1:为什么设计师给的设计稿总是640px或750px(现在一般以Phone6为基准,给的750px) 问题2:为什么我 ...

  9. 学习Python一年,这次终于弄懂了浅拷贝和深拷贝

    官方文档:copy主题 源代码: Lib/copy.py 话说,网上已经有很多关于Python浅拷贝和深拷贝的文章了,不过好多文章看起来还是决定似懂非懂,所以决定用自己的理解来写出这样一篇文章. 当别 ...

随机推荐

  1. Linux入门基础之 下

    八.Linux 管道.重定向及文本处理 8.1.Linux 多命令协作:管道及重定向 8.1.1 开源文化 开源文化的核心理念之一就是不要重复发明轮子,很多的开源软件都是现有软件.代码.功能的重新组合 ...

  2. Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码

    在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...

  3. docker容器添加对外映射端口

    一般在运行容器时,我们都会通过参数 -p(使用大写的-P参数则会随机选择宿主机的一个端口进行映射)来指定宿主机和容器端口的映射,例如 docker run -it -d --name [contain ...

  4. 阿里云ECS服务器提示需要修复的漏洞问题

    1.漏洞: RHSA-2018:1842: kernel security, bug fix, and enhancement updateRHSA-2018:2299: NetworkManager ...

  5. 序列标注(BiLSTM-CRF/Lattice LSTM)

    前言 在三大特征提取器中,我们已经接触了LSTM/CNN/Transormer三种特征提取器,这一节我们将介绍如何使用BiLSTM实现序列标注中的命名实体识别任务,以及Lattice-LSTM的模型原 ...

  6. git clone remote: HTTP Basic: Access denied

    git clone 项目失败,报下面的错误信息: $ git clone http://192.168.0.141/xxxx.git Cloning into 'appEnterprise'... r ...

  7. 43 道检验基础的 JavaScript 面试题

    导读 这两天的GitHub Trending repositories被一个名叫 javascript-questions的项目霸榜了,项目中记录了一些JavaScript题目. 文中有些点作者解释的 ...

  8. 关于SpringBoot 1.x和2.x版本差别

    有点小差别 基本上基于SpringBoot的代码不需要改动,但有些配置属性和配置类,可能要改动,改动原因是 配置和类的更新或者是改名一般正常的MVC,数据库访问这些都不需要改动,下面按照本书章节说明区 ...

  9. 基于.NET环境,C#语言 实现 TCP NAT

    基于.NET平台和C#语言实现TCP NAT穿越        1.<C# WinForm 跨线程访问控件(实用简洁写法)>       2.<基于.NET环境,C#语言 实现 TC ...

  10. @DateTimeFormat注解

    @DateTimeFormat在spring-context依赖下,所在包如下 当form表单中出现时间字段需要跟pojo对象中的成员变量进行数据绑定时,springmvc框架中的时间数据无法自动绑定 ...