小白终于弄懂了:c#从async/await到Task再到Thread

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秒。




 




posted @ 2019-08-01 22:26  HeyJudeee  阅读(8843)  评论(2编辑  收藏  举报