中抓,笑傲天下之邪神丁典,阎德利
本来这篇文章上个月就该发布了,但是因为忙 quarkdoc 一直没有时间整理,所以耽搁到今天,现在回归正轨。
c# 5.0 虽然只引入了2个新关键词:async和await。然而它大大简化了异步方法的编程。
在 线程池(threadpool)大致介绍了微软在不同时期使用的不同的异步模式,有3种:
1.异步模式
2.基于事件的异步模式
3.基于任务的异步模式(tap)
而最后一种就是利用async和await关键字来实现的(tap是现在微软极力推崇的一种异步编程方式)。
但请谨记,async和await关键字只是编译器功能。编译器会用task类创建代码。如果不使用这两个关键词,用c#4.0的task类同样可以实现相同的功能,只是没有那么方便而已。
使用async和await关键词编写异步代码,具有与同步代码相当的结构和简单性,并且摒弃了异步编程的复杂结构。
但是在理解上刚开始会很不习惯,而且会把一些情况想当然了,而真实情况会相去甚远(我犯过这样的错误)。所以根据几个示例一步步理解更加的靠谱些。
这是一个简单的同步方法调用示例:
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline($"头部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 6 string result = sayhi("jack"); 7 console.writeline(result); 8 console.writeline($"尾部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 9 console.readkey(); 10 } 11 static string sayhi(string name) 12 { 13 task.delay(2000).wait();//异步等待2s 14 console.writeline($"sayhi执行,当前线程id为:{thread.currentthread.managedthreadid}"); 15 return $"hello,{name}"; 16 } 17 }
执行结果如下,方法在主线程中运行,主线程被阻塞。
示例将方法放到任务内执行:
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline($"头部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 6 string result = sayhiasync("jack").result; 7 console.writeline(result); 8 console.writeline($"尾部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 9 console.readkey(); 10 } 11 static task<string> sayhiasync(string name) 12 { 13 return task.run<string>(() => { return sayhi(name); }); 14 } 15 static string sayhi(string name) 16 { 17 task.delay(2000).wait();//异步等待2s 18 console.writeline($"sayhi执行,当前线程id为:{thread.currentthread.managedthreadid}"); 19 return $"hello,{name}"; 20 } 21 }
执行结果如下,方法在另外一个线程中运行,因为主线程调用了result,result在任务没有完成时内部会使用wait,所以主线程还是会被阻塞。
示例为了避免阻塞主线程使用任务延续的方式:
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline($"头部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 6 task<string> task = sayhiasync("jack"); 7 task.continuewith(t =>//延续任务,指定任务执行完成后延续的操作 8 { 9 console.writeline($"延续执行,当前线程id为:{thread.currentthread.managedthreadid}"); 10 string result = t.result; 11 console.writeline(result); 12 }); 13 console.writeline($"尾部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 14 console.readkey(); 15 } 16 static task<string> sayhiasync(string name) 17 { 18 return task.run<string>(() => { return sayhi(name); }); 19 } 20 static string sayhi(string name) 21 { 22 task.delay(2000).wait();//异步等待2s 23 console.writeline($"sayhi执行,当前线程id为:{thread.currentthread.managedthreadid}"); 24 return $"hello,{name}"; 25 } 26 }
执行结果如下,方法在另外一个线程中运行,因为任务附加了延续,延续会在任务完成后处理返回值,而主线程不会被阻塞。这应该就是想要的效果了。
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline($"头部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 6 callerwithasync("jack"); 7 console.writeline($"尾部已执行,当前主线程id为:{thread.currentthread.managedthreadid}"); 8 console.readkey(); 9 } 10 async static void callerwithasync(string name) 11 { 12 console.writeline($"异步调用头部执行,当前线程id为:{thread.currentthread.managedthreadid}"); 13 string result = await sayhiasync(name); 14 console.writeline($"异步调用尾部执行,当前线程id为:{thread.currentthread.managedthreadid}"); 15 console.writeline(result); 16 } 17 static task<string> sayhiasync(string name) 18 { 19 return task.run<string>(() => { return sayhi(name); }); 20 } 21 static string sayhi(string name) 22 { 23 task.delay(2000).wait();//异步等待2s 24 console.writeline($"sayhi执行,当前线程id为:{thread.currentthread.managedthreadid}"); 25 return $"hello,{name}"; 26 } 27 }
执行结果如下,使用await关键字来调用返回任务的异步方法sayhiasync,而使用await需要有用async修饰符声明的方法,在sayhiasync方法为完成前,下面的方法不会继续执行。但是主线程并没有阻塞,且任务处理完成后await后的逻辑继续执行。
本质:编译器将await关键字后的所有代码放进了延续(continuewith)方法的代码块中来转换await关键词。
使用async修饰符标记的方法称为异步方法,异步方法只可以具有以下返回类型:
1.task
2.task<tresult>
3.void
4.从c# 7.0开始,任何具有可访问的getawaiter方法的类型。system.threading.tasks.valuetask<tresult> 类型属于此类实现(需向项目添加system.threading.tasks.extensions
nuget 包)。
异步方法通常包含 await 运算符的一个或多个实例,但缺少 await 表达式也不会导致生成编译器错误。 如果异步方法未使用 await 运算符标记暂停点,那么异步方法会作为同步方法执行,即使有 async 修饰符也不例外,编译器将为此类方法发布一个警告。
await 表达式只能在由 async 修饰符标记的封闭方法体、lambda 表达式或异步方法中出现。在其他位置,它会解释为标识符。
使用await运算符的任务只可用于返回 task、task<tresult> 和 system.threading.tasks.valuetype<tresult> 对象的方法。
异步方法同步运行,直至到达其第一个 await 表达式,此时await在方法的执行中插入挂起点,会将方法挂起直到所等待的任务完成,然后继续执行await后面的代码区域。
await 表达式并不阻止正在执行它的线程。 而是使编译器将剩下的异步方法注册为等待任务的延续任务。 控制权随后会返回给异步方法的调用方。 任务完成时,它会调用其延续任务,异步方法的执行会在暂停的位置处恢复。
注意:
1.无法等待具有 void 返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法抛出的任何异常。
2.异步方法无法声明 in、ref 或 out 参数,但可以调用包含此类参数的方法。 同样,异步方法无法通过引用返回值,但可以调用包含 ref 返回值的方法。
异步编程中最需弄清的是控制流是如何从方法移动到方法的。
下列示例及说明引自(),个人认为已经很清晰了:
1 class program 2 { 3 static void main(string[] args) 4 { 5 var result = accessthewebasync(); 6 console.readkey(); 7 } 8 async static task<int> accessthewebasync() 9 { 10 httpclient client = new httpclient(); 11 // getstringasync返回一个任务。任务result会得到一个字符串(urlcontents)。 12 task<string> getstringtask = client.getstringasync("https://www.cnblogs.com/jonins/"); 13 //您可以在这里完成不依赖于getstringasync的字符串的工作。 14 doindependentwork(); 15 //等待的操作员暂停进入webasync。 16 //accessthewebasync在getstringtask完成之前不能继续。 17 //同时,控制权返回到accessthewebasync的调用方。 18 //当getstringtask完成后,控件权将继续在这里工作。 然后,await运算符从getstringtask检索字符串结果。 19 string urlcontents = await getstringtask; 20 //任务完成 21 console.writeline(urlcontents.length); 22 //return语句指定一个整数结果。 23 return urlcontents.length; 24 } 25 static void doindependentwork() 26 { 27 console.writeline("working.........."); 28 } 29 }
在一个异步方法里,可以调用一个或多个异步方法,如何编码取决于异步方法间结果是否相互依赖。
使用await关键词可以调用每个异步方法,如果一个异步方法需要使用另一个异步方法的结果,await关键词就非常必要。
示例如下:
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline("执行前....."); 6 getresultasync(); 7 console.writeline("执行中....."); 8 console.readkey(); 9 } 10 async static void getresultasync() 11 { 12 var number1 = await getresult(10); 13 var number2 = getresult(number1); 14 console.writeline($"结果分别为:{number1}和{number2.result}"); 15 } 16 static task<int> getresult(int number) 17 { 18 return task.run<int>(() => { task.delay(1000).wait(); return number + 10; }); 19 } 20 }
如果异步方法间相互不依赖,则每个异步方法都不使用await,而是把每个异步方法的结果赋值给task变量,就会运行得更快。
示例如下:
1 class program 2 { 3 static void main(string[] args) 4 { 5 console.writeline("执行前....."); 6 getresultasync(); 7 console.writeline("执行中....."); 8 console.readkey(); 9 } 10 async static void getresultasync() 11 { 12 task<int> task1 = getresult(10); 13 task<int> task2 = getresult(20); 14 await task.whenall(task1, task2); 15 console.writeline($"结果分别为:{task1.result}和{task2.result}"); 16 } 17 static task<int> getresult(int number) 18 { 19 return task.run<int>(() => { task.delay(1000).wait(); return number + 10; }); 20 } 21 }
task类定于2个组合器分别为:whenall和whenany。
whenall是在所有传入的任务都完成时才返回task。
whenany是在传入的任务其中一个完成就会返回task。
以下示例一种是普通的错误的捕获方式,另一种是异步方法异常捕获方式:
1 class program 2 { 3 static void main(string[] args) 4 { 5 6 donthandle(); 7 handleerror(); 8 console.readkey(); 9 } 10 //错误处理 11 static void donthandle() 12 { 13 try 14 { 15 var task = throwafter(0, "donthandle error"); 16 } 17 catch (exception ex) 18 { 19 20 console.writeline(ex.message); 21 } 22 } 23 //异步方法错误处理 24 static async void handleerror() 25 { 26 try 27 { 28 await throwafter(2000, "handleerror error"); 29 } 30 catch (exception ex) 31 { 32 33 console.writeline(ex.message); 34 } 35 } 36 //在延迟后抛出异常 37 static async task throwafter(int ms, string message) 38 { 39 await task.delay(ms); 40 throw new exception(message); 41 } 42 }
执行结果如下:
调用异步方法,如果只是简单的放在try/catch块中,将会捕获不到异常。这是因为donthandle方法在throwafter抛出异常之前已经执行完毕(返回void的异步方法不会等待。这是因为从async void方法抛出的异常无法捕获。因此异步方法最好返回一个task类型)。
异步方法的一个较好异常处理方式,是使用await关键字,将其放在try/catch中。
如果调用了多个异步方法,在第一个异步方法抛出异常,后续的方法将不会被调用,catch块内只会处理出现的第一个异常。
所以正确的做法是使用task.whenall,不管任务是否抛出异常都会等到所有任务完成。task.whenall结束后,异常被catch语句捕获到。如果只是捕获exception,我们只能看到whenall方法的第一个发生异常的任务信息,不会抛出后续的异常任务。
如果要捕获所有任务的异常信息,就是对任务声明变量,在catch块内可以访问,再使用isfaulted属性检查任务的状态,以确认它们是否出现错误,然后再进行处理。示例如下:
1 class program 2 { 3 static void main(string[] args) 4 { 5 handleerror(); 6 console.readkey(); 7 } 8 //正确的处理方式 9 static async void handleerror() 10 { 11 task t1 = null; 12 task t2 = null; 13 try 14 { 15 t1 = throwafter(1000, "handleerror-one-error"); 16 t2 = throwafter(2000, "handleerror-two-error"); 17 await task.whenall(t1, t2); 18 } 19 catch (exception) 20 { 21 if (t1.isfaulted) 22 console.writeline(t1.exception.innerexception.message); 23 if (t2.isfaulted) 24 console.writeline(t2.exception.innerexception.message); 25 } 26 } 27 //在延迟后抛出异常 28 static async task throwafter(int ms, string message) 29 { 30 await task.delay(ms); 31 throw new exception(message); 32 } 33 }
在 中介绍过aggregateexception,它包含了等待中所有异常的列表,可轻松遍历处理所有异常信息。示例如下:
1 class program 2 { 3 static void main(string[] args) 4 { 5 handleerror(); 6 console.readkey(); 7 } 8 //正确的处理方式 9 static async void handleerror() 10 { 11 task taskresult = null; 12 try 13 { 14 task t1 = throwafter(1000, "handleerror-one-error"); 15 task t2 = throwafter(2000, "handleerror-two-error"); 16 await (taskresult = task.whenall(t1, t2)); 17 } 18 catch (exception) 19 { 20 foreach (var ex in taskresult.exception.innerexceptions) 21 { 22 console.writeline(ex.message); 23 } 24 25 } 26 } 27 //在延迟后抛出异常 28 static async task throwafter(int ms, string message) 29 { 30 await task.delay(ms); 31 throw new exception(message); 32 } 33 }
.net有很多异步api我们都可以通过async/await构建调用提高响应能力,例如:
1 class program 2 { 3 static void main(string[] args) 4 { 5 demo(); 6 console.readkey(); 7 } 8 static async void demo() 9 { 10 httpclient httpclient = new httpclient(); 11 var gettaskresult = await httpclient.getstringasync("https://www.cnblogs.com/jonins/"); 12 console.writeline(gettaskresult); 13 } 14 }
这些api都有相同原则即以async结尾。
1.async方法需在其主体中具有await 关键字,否则它们将永不暂停。同时c# 编译器将生成一个警告,此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 c# 编译器为异步方法生成的状态机将不会完成任何任务。
2.应将“async”作为后缀添加到所编写的每个异步方法名称中。这是 .net 中的惯例,以便更轻松区分同步和异步方法。
3.async void 应仅用于事件处理程序。因为事件不具有返回类型(因此无法返回 task 和 task<t>)。 其他任何对 async void 的使用都不遵循 tap 模型,且可能存在一定使用难度。
例如:async void 方法中引发的异常无法在该方法外部被捕获或十分难以测试 async void 方法。
异步编程的准则是确定所需执行的操作是i/o-bound还是 cpu-bound。因为这会极大影响代码性能,并可能导致某些构造的误用。
考虑两个问题:
1.你的代码是否会“等待”某些内容,例如数据库中的数据或web资源等?如果答案为“是”,则你的工作是 i/o-bound。
2.你的代码是否要执行开销巨大的计算?如果答案为“是”,则你的工作是 cpu-bound。
如果你的工作为 i/o-bound,请使用 async 和 await(而不使用 task.run)。 不应使用任务并行库。
如果你的工作为 cpu-bound,并且你重视响应能力,请使用 async 和 await,并在另一个线程上使用 task.run 生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
如果想要了解状态机请戳:这里 。
c#高级编程(第10版) c# 6 & .net core 1.0 christian nagel
果壳中的c# c#5.0权威指南 joseph albahari
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/await
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
Blazor server side 自家的一些开源的, 实用型项目的进度之 CEF客户端
.NET IoC模式依赖反转(DIP)、控制反转(Ioc)、依赖注入(DI)
vue+.netcore可支持业务代码扩展的开发框架 VOL.Vue 2.0版本发布
网友评论