第⼗五节:深⼊理解async和await的作⽤及各种适⽤场景和⽤法(旧,详见
最新两篇)
1. 同步 VS 异步 VS 多线程
同步⽅法:调⽤时需要等待返回结果,才可以继续往下执⾏业务
异步⽅法:调⽤时⽆须等待返回结果,可以继续往下执⾏业务
开启新线程:在主线程之外开启⼀个新的线程去执⾏业务
同步⽅法和异步⽅法的本质区别:调⽤时是否需要等待返回结果才能继续执⾏业务
2. 常见的异步⽅法(都以Async结尾)
① HttpClient类:PostAsync、PutAsync、GetAsync、DeleteAsync
② EF中DbContext类:SaveChangesAsync
③⽂件相关中的:WriteLineAsync
3. 引⼊异步⽅法的背景
⽐如我在后台要向另⼀台服务器中获取中的2个接⼝获取信息,然后将两个接⼝的信息拼接起来,⼀起输出,接⼝1耗时3s,接⼝2耗时5s,
①传统的同步⽅式:
需要的时间⼤约为:3s + 5s =8s, 如下⾯【案例1】
先分享⼀个同步请求接⼝的封装⽅法,下同。
1public class HttpService
2 {
3///<summary>
4///后台跨域请求发送代码
5///</summary>
6///<param name="url">eg:/jeesite/regist/saveAppAgentAccount</param>
7///<param name="postData"></param>
8///参数格式(⼿拼Json) string postData = "{\"name\":\"" + vipName + "\",\"shortName\":\"" + vip.shortName + + "\"}";
9///<returns></returns>
10public static string PostData(string postData, string url)
11 {
12 HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);//后台请求页⾯
13 Encoding encoding = Encoding.GetEncoding("utf-8");//注意页⾯的编码,否则会出现乱码
14byte[] requestBytes = encoding.GetBytes(postData);writeline方法的作用
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"));
23string backstr = sr.ReadToEnd();//可以读取到从页⾯返回的结果,以数据流的形式。
24 sr.Close();
25 res.Close();
26
27return backstr;
28 }
View Code
然后在分享服务上的耗时操作,下同。
1///<summary>
2///耗时⽅法耗时3s
3///</summary>
4///<returns></returns>
5public ActionResult GetMsg1()
6 {
7 Thread.Sleep(3000);
8return Content("GetMsg1");
9
10 }
11
12///<summary>
13///耗时⽅法耗时5s
14///</summary>
15///<returns></returns>
16public ActionResult GetMsg2()
17 {
18 Thread.Sleep(5000);
19return Content("GetMsg2");
20
21 }
View Code
下⾯是案例1代码
1 #region案例1(传统同步⽅式耗时8s左右)
2 {
3 Stopwatch watch = Stopwatch.StartNew();
4 Console.WriteLine("开始执⾏");
5
6string t1 = HttpService.PostData("", "localhost:2788/Home/GetMsg1");
7string t2 = HttpService.PostData("", "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
6var task1 = Task.Run(() =>
7 {
8return HttpService.PostData("", "localhost:2788/Home/GetMsg1");
9 });
10
11var task2 = Task.Run(() =>
12 {
13return HttpService.PostData("", "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();
5var httpContent = new StringContent("", Encoding.UTF8, "application/json");
6 Console.WriteLine("开始执⾏");
7//执⾏业务
8var r1 = http.PostAsync("localhost:2788/Home/GetMsg1", httpContent);
9var r2 = http.PostAsync("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结尾的异步⽅法)
1//利⽤async封装同步业务的⽅法
2private static async Task<string> NewMethod5Async()
3 {
4 Thread.Sleep(3000);
5//其它同步业务
6return"Msg1";
7 }
8private static async Task<string> NewMethod6Async()
9 {
10 Thread.Sleep(5000);
11//其它同步业务
12return"Msg2";
13 }
View Code
1#region案例4(async关键字封装的⽅法中如果写全部都是⼀些串⾏业务耗时8s左右)
2 {
3 Stopwatch watch = Stopwatch.StartNew();
4
5 Console.WriteLine("开始执⾏");
6
7 Task<string> t1 = NewMethod5Async();
8 Task<string> t2 = NewMethod6Async();
9
10 Console.WriteLine("我是主业务");
11 Console.WriteLine($"{t1.Result},{t2.Result}");
12 watch.Stop();
13 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
14 }
15#endregion
观点结论1:从上⾯③中可以得出⼀个结论,async中必须要有await运算符才能起到异步⽅法的作⽤,且await 运算符只能加在系统类库默认提供的异步⽅法或者新线程(如:Task.Run)前⾯。
如:下⾯【案例5】和【案例6】需要的时间⼤约为:Max(3s,5s) = 5s
1// 将系统类库提供的异步⽅法利⽤async封装起来
2private static async Task<String> NewMethod1Async()
3 {
4 HttpClient http = new HttpClient();
5var httpContent = new StringContent("", Encoding.UTF8, "application/json");
6//执⾏业务
7var r1 = await http.PostAsync("localhost:2788/Home/GetMsg1", httpContent);
8return r1.Content.ReadAsStringAsync().Result;
9 }
10private static async Task<String> NewMethod2Async()
11 {
12 HttpClient http = new HttpClient();
13var httpContent = new StringContent("", Encoding.UTF8, "application/json");
14//执⾏业务
15var r1 = await http.PostAsync("localhost:2788/Home/GetMsg2", httpContent);
16return r1.Content.ReadAsStringAsync().Result;
18
19//将await关键字加在新线程的前⾯
20private static async Task<string> NewMethod3Async()
21 {
22var msg = await Task.Run(() =>
23 {
24return HttpService.PostData("", "localhost:2788/Home/GetMsg1");
25 });
26return msg;
27 }
28private static async Task<string> NewMethod4Async()
29 {
30var msg = await Task.Run(() =>
31 {
32return HttpService.PostData("", "localhost:2788/Home/GetMsg2");
33 });
34return msg;
35 }
View Code
1 #region案例5(将系统类库提供的异步⽅法利⽤async封装起来耗时5s左右)
2//并且先输出“我是主业务”,证明t1和t2是并⾏执⾏的,且不阻碍主业务
3 {
4 Stopwatch watch = Stopwatch.StartNew();
5
6 Console.WriteLine("开始执⾏");
7 Task<string> t1 = NewMethod1Async();
8 Task<string> t2 = NewMethod2Async();
9
10 Console.WriteLine("我是主业务");
11 Console.WriteLine($"{t1.Result},{t2.Result}");
12 watch.Stop();
13 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
14 }
15#endregion
1 #region案例6(将新线程利⽤async封装起来耗时5s左右)
2//并且先输出“我是主业务”,证明t1和t2是并⾏执⾏的,且不阻碍主业务
3 {
4 Stopwatch watch = Stopwatch.StartNew();
5
6 Console.WriteLine("开始执⾏");
7 Task<string> t1 = NewMethod3Async();
8 Task<string> t2 = NewMethod4Async();
9
10 Console.WriteLine("我是主业务");
11 Console.WriteLine($"{t1.Result},{t2.Result}");
12 watch.Stop();
13 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
14 }
15#endregion
2. ⼏个规则和约定
① async封装的⽅法中,可以有多个await,这⾥的await代表等待该⾏代码执⾏完毕。
②我们通常⾃⼰封装的⽅法也要以Async结尾,⽅便识别
③异步返回类型主要有三种:Task<T> 、Task、Void
3. 测试得出其他⼏个结论
①如果async封装的异步⽅法⾥既有同步业务⼜有异步业务(开启新线程或者系统类库提供异步⽅法),
那么同步⽅法那部分的时间在调⽤的时候是会阻塞主线程的,即主线程要等待这部分同步业务执⾏完才能往下执⾏。
如【案例7】耗时:同步操作之和 2s+2s + Max(3s,5s)=9s;
1//同步耗时操作和异步⽅法同时封装
2private static async Task<String> NewMethod7Async()
3 {
4//调⽤异步⽅法之前还有⼀个耗时操作
5 Thread.Sleep(2000);
6
7//下⾯的操作耗时3s
8 HttpClient http = new HttpClient();
9var httpContent = new StringContent("", Encoding.UTF8, "application/json");
10//执⾏业务
11var r1 = await http.PostAsync("localhost:2788/Home/GetMsg1", httpContent);
12return r1.Content.ReadAsStringAsync().Result;
13 }
14private static async Task<String> NewMethod8Async()
16//调⽤异步⽅法之前还有⼀个耗时操作
17 Thread.Sleep(2000);
18
19//下⾯的操作耗时5s
20 HttpClient http = new HttpClient();
21var httpContent = new StringContent("", Encoding.UTF8, "application/json");
22//执⾏业务
23var r1 = await http.PostAsync("localhost:2788/Home/GetMsg2", httpContent);
24return r1.Content.ReadAsStringAsync().Result;
25 }
View Code
1 #region案例7(既有普通的耗时操作,也有系统本⾝的异步⽅法,耗时9s左右)
2//且⼤约4s后才能输出 “我是主业务”,证明同步操作Thread.Sleep(2000); 阻塞主线程
3 {
4 Stopwatch watch = Stopwatch.StartNew();
5
6 Console.WriteLine("开始执⾏");
7 Task<string> t1 = NewMethod7Async();
8 Task<string> t2 = NewMethod8Async();
9
10 Console.WriteLine("我是主业务");
11 Console.WriteLine($"{t1.Result},{t2.Result}");
12 watch.Stop();
13 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
14 }
15#endregion
证明:async封装的异步⽅法⾥的同步业务的时间会阻塞主线程,再次证明 await只能加在⾮阻⽌api和开启新线程的前⾯
②如果封装的异步⽅法中存在等待的问题,⽽且不能阻塞主线程(不能⽤Thread.Sleep) , 这个时候可以⽤Task.Delay,并在前⾯加await关键字 如【案例8】耗时:Max(2+3 , 5+2)=7s
1//利⽤Task.Delay(2000);等待
2private static async Task<String> NewMethod11Async()
3 {
4//调⽤异步⽅法之前需要等待2s
5await Task.Delay(2000);
6
7//下⾯的操作耗时3s
8 HttpClient http = new HttpClient();
9var httpContent = new StringContent("", Encoding.UTF8, "application/json");
10//执⾏业务
11var r1 = await http.PostAsync("localhost:2788/Home/GetMsg1", httpContent);
12return r1.Content.ReadAsStringAsync().Result;
13 }
14
15private static async Task<String> NewMethod12Async()
16 {
17//调⽤异步⽅法之前需要等待2s
18await Task.Delay(2000);
19
20//下⾯的操作耗时5s
21 HttpClient http = new HttpClient();
22var httpContent = new StringContent("", Encoding.UTF8, "application/json");
23//执⾏业务
24var r1 = await http.PostAsync("localhost:2788/Home/GetMsg2", httpContent);
25return r1.Content.ReadAsStringAsync().Result;
26 }
View Code
1 #region案例8(利⽤Task.Delay执⾏异步⽅法的等待操作)
2//结果是7s,且马上输出“我是主业务”,说明Task.Delay(),不阻塞主线程。
3 {
4 Stopwatch watch = Stopwatch.StartNew();
5 Console.WriteLine("开始执⾏");
6 Task<string> t1 = NewMethod11Async();
7 Task<string> t2 = NewMethod12Async();
8
9 Console.WriteLine("我是主业务");
10 Console.WriteLine($"{t1.Result},{t2.Result}");
11 watch.Stop();
12 Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}");
13 }
14#endregion
三. 异步⽅法返回类型
1. Task<T>, 处理含有返回值的异步⽅法,通过 .Result 等待异步⽅法执⾏完,且获取到返回值。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论