第十五节:深入理解async和await的作用及各种适用场景和用法 那些年我们一起追逐的多线程(Thread、ThreadPool、委托异步调用、Task/TaskFactory、Parallerl、async和await)

一. 同步VS异步

1.   同步 VS 异步 VS 多线程

同步方法:需要等待返回结果,才可以继续往下执行业务
异步方法:无须等待返回结果,可以继续往下执行业务
开启新线程:在主线程之外开启一个新的线程去执行业务
同步方法和异步方法的本质区别: 是否需要等待返回结果才能继续执行业务
自带的异步方法和多线程区别:多线程指开启了新的线程,而系统类库自带异步方法并没有开启新线程

2. 常见的异步方法(都以Async结尾)

  ① HttpClient类:PostAsync、PutAsync、GetAsync、DeleteAsync

  ② EF中DbContext类:SaveChangesAsync

  ③ 文件相关中的:WriteLineAsync

  观点结论1:以上这些系统自身封装的异步方法是不开启新线程的。

3. 引入异步方法的背景

  比如我在后台要向另一台服务器中获取中的2个接口获取信息,然后将两个接口的信息拼接起来,一起输出,接口1耗时3s,接口2耗时5s,

① 传统的同步方式:

  需要的时间大约为:3s + 5s =8s, 如下面 【案例1】

先分享一个同步请求接口的封装方法,下同。

 1   public class HttpService
 2     {
 3         /// <summary>
 4         /// 后台跨域请求发送代码
 5         /// </summary> 
 6         /// <param name="url">eg:http://ac.guojin.org/jeesite/regist/saveAppAgentAccount </param>
 7         ///<param name="postData"></param>
 8         ///  参数格式(手拼Json) string postData = "{\"name\":\"" + vip.comName + "\",\"shortName\":\"" + vip.shortName + + "\"}";             
 9         /// <returns></returns>
10         public static string PostData(string postData, string url)
11         {
12             HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);//后台请求页面
13             Encoding encoding = Encoding.GetEncoding("utf-8");//注意页面的编码,否则会出现乱码
14             byte[] requestBytes = encoding.GetBytes(postData);
15             req.Method = "POST";
16             req.ContentType = "application/json";
17             req.ContentLength = requestBytes.Length;
18             Stream requestStream = req.GetRequestStream();
19             requestStream.Write(requestBytes, 0, requestBytes.Length);
20             requestStream.Close();
21             HttpWebResponse res = (HttpWebResponse)req.GetResponse();
22             StreamReader sr = new StreamReader(res.GetResponseStream(), System.Text.Encoding.GetEncoding("utf-8"));
23             string backstr = sr.ReadToEnd();//可以读取到从页面返回的结果,以数据流的形式。
24             sr.Close();
25             res.Close();
26 
27             return backstr;
28         }
View Code

然后在分享服务上的耗时操作,下同。

 1  /// <summary>
 2         /// 耗时方法  耗时3s
 3         /// </summary>
 4         /// <returns></returns>
 5         public ActionResult GetMsg1()
 6         {
 7             Thread.Sleep(3000);
 8             return Content("GetMsg1");
 9 
10         }
11 
12         /// <summary>
13         /// 耗时方法  耗时5s
14         /// </summary>
15         /// <returns></returns>
16         public ActionResult GetMsg2()
17         {
18             Thread.Sleep(5000);
19             return Content("GetMsg2");
20 
21         }
View Code

下面是案例1代码

 1        #region 案例1(传统同步方式 耗时8s左右)
 2             {
 3                 Stopwatch watch = Stopwatch.StartNew();
 4                 Console.WriteLine("开始执行");
 5 
 6                 string t1 = HttpService.PostData("", "http://localhost:2788/Home/GetMsg1");
 7                 string t2 = HttpService.PostData("", "http://localhost:2788/Home/GetMsg2");
 8 
 9                 Console.WriteLine("我是主业务");
10                 Console.WriteLine($"{t1},{t2}");
11                 watch.Stop();
12                 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
13             }
14             #endregion

② 开启新线程分别执行两个耗时操作

  需要的时间大约为:Max(3s,5s) = 5s ,如下面【案例2】

 1         #region 案例2(开启新线程分别执行两个耗时操作 耗时5s左右)
 2             {
 3                 Stopwatch watch = Stopwatch.StartNew();
 4                 Console.WriteLine("开始执行");
 5 
 6                 var task1 = Task.Run(() =>
 7                 {
 8                     return HttpService.PostData("", "http://localhost:2788/Home/GetMsg1");
 9                 });
10 
11                 var task2 = Task.Run(() =>
12                 {
13                     return HttpService.PostData("", "http://localhost:2788/Home/GetMsg2");
14                 });
15 
16                 Console.WriteLine("我是主业务");
17                 //主线程进行等待
18                 Task.WaitAll(task1, task2);
19                 Console.WriteLine($"{task1.Result},{task2.Result}");
20                 watch.Stop();
21                 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
22             }
23             #endregion

  既然②方式可以解决同步方法串行耗时间的问题,但这种方式存在一个弊端,一个业务中存在多个线程,且需要对线程进行管理,相对麻烦,从而引出了异步方法。

这里的异步方法 我 特指:系统类库自带的以async结尾的异步方法。

③ 使用系统类库自带的异步方法

  需要的时间大约为:Max(3s,5s) = 5s ,如下面【案例3】

 1       #region 案例3(使用系统类库自带的异步方法 耗时5s左右)
 2             {
 3                 Stopwatch watch = Stopwatch.StartNew();
 4                 HttpClient http = new HttpClient();
 5                 var httpContent = new StringContent("", Encoding.UTF8, "application/json");
 6                 Console.WriteLine("开始执行");
 7                 //执行业务
 8                 var r1 = http.PostAsync("http://localhost:2788/Home/GetMsg1", httpContent);
 9                 var r2 = http.PostAsync("http://localhost:2788/Home/GetMsg2", httpContent);
10                 Console.WriteLine("我是主业务");
11 
12                 //通过异步方法的结果.Result可以是异步方法执行完的结果
13                 Console.WriteLine(r1.Result.Content.ReadAsStringAsync().Result);
14                 Console.WriteLine(r2.Result.Content.ReadAsStringAsync().Result);
15 
16                 watch.Stop();
17                 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
18             }
19             #endregion

PS:通过 .Result 来获取异步方法执行完后的结果。

二. 利用async和await封装异步方法

1. 首先要声明几点:

  ① async和await关键字是C# 5.0时代引入的,它是一种异步编程模型

  ② 它们本身并不创建新线程,但我可以在自行封装的async中利用Task.Run开启新线程

  ③ 利用async关键字封装的方法中如果写全部都是一些串行业务,且不用await关键字,那么即使使用async封装,也并没有什么卵用,并起不了异步方法的作用。

   需要的时间大约为:3s + 5s =8s, 如下面 【案例4】,并且封装的方法编译器会提示:“缺少关键字await,将以同步的方式调用,请使用await运算符等待非阻止API或Task.Run的形式”(PS:非阻止API指系统类库自带的以Async结尾的异步方法)

  观点结论2:从上面③中可以得出一个结论,async中必须要有await运算符才能起到异步方法的作用,且await 运算符只能加在 系统类库默认提供的异步方法或者新线程(如:Task.Run)前面。 如:下面【案例5】 和 【案例6】需要的时间大约为:Max(3s,5s) = 5s

2. 几个规则和约定

  ① async封装的方法中,可以有多个await,这里的await代表等待该行代码执行完毕。

  ② 我们通常自己封装的方法也要以Async结尾,方便识别

  ③ 异步返回类型主要有三种:Task<T> 、Task、Void

3. 测试得出其他几个结论

① 如果async封装的异步方法里既有同步业务又有异步业务(开启新线程或者系统类库提供异步方法),那么同步方法那部分的时间在调用的时候是会阻塞主线程的,即主线程要等待这部分同步业务执行完才能往下执行。

  如【案例7】 耗时:同步操作之和 2s+2s + Max(3s,5s)=9s;

   证明:async封装的异步方法里的同步业务的时间会阻塞主线程,再次证明 await只能加在 非阻止api和开启新线程的前面

② 如果封装的异步方法中存在等待的问题,而且不能阻塞主线程(不能用Thread.Sleep),这个时候可以用Task.Delay,并在前面加await关键字

  如【案例8】 耗时:Max(2+3,5+2)=7s

二. 常用语法糖

二. 常用语法糖

五. 参考资料

   1. 反骨仔:http://www.cnblogs.com/liqingwen/p/5831951.html

        http://www.cnblogs.com/liqingwen/p/5844095.html

  2. MSDN:https://msdn.microsoft.com/library/hh191443(vs.110).aspx

PS:如果你想了解多线程的其他知识,请移步:那些年我们一起追逐的多线程(Thread、ThreadPool、委托异步调用、Task/TaskFactory、Parallerl、async和await)

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,如需代码请加我QQ:604649488 (备注:评论的博客名)
 

猜你喜欢

转载自www.cnblogs.com/yaopengfei/p/9249390.html