.Net技术栈下的异步,你还在用同步方式进行开发吗?
郑小超 人气:0关于异步,其实是个老生常谈的话题,也是各大公司面试常问的问题之一.本文就几个点来介绍异步解决的问题
注:对多线程的运行的基本机制要了解
1、介绍
有人可能会有疑问,为什么并行,非得用异步.多线程也已可以啊,多开两个线程不就行了.
案例分析:现在有一个生活场景.需要煮饭(假设需要20分钟-机器煮)、洗衣服(假设需要25分钟-机器洗)、蒸菜(假设需要10分钟-机器蒸).并且昨晚之后我需要告知哥们.
(1)、同步
如果采用同步的方式(我先煮饭,把米放到电饭煲里,然后站在电饭煲前面等,什么事都不能干,一直等到饭煮好.接下去在洗衣服....以此类推),那么我需要一件一件做,那么我总共要花费20+25+10=55分钟.显然这种方式很蠢.实例代码如下:
class Program { static void Main(string[] args) { CookRice(); CookDish(); DoLaundry(); Console.WriteLine("哥们,全都搞定了"); Console.ReadKey(); } /// <summary> /// 煮饭 /// </summary> static void CookRice() { Thread.Sleep(20 * 1000 * 60); } /// <summary> /// 蒸菜 /// </summary> static void CookDish() { Thread.Sleep(10 * 1000 * 60); } /// <summary> /// 洗衣服 /// </summary> static void DoLaundry() { Thread.Sleep(25 * 1000 * 60); } }
(2)、多线程优化
ok,上面的代码执行方式显然是很蠢的,因为那样会占用我(线程)大量的时间.
注意:这里要从执行时间这个角度去考虑,因为作为Web开发,一个用户请求从开始到结束,是有时间限制的,如果你一个请求(报表之类的特殊业务除外,指互联网场景)超过1秒,用户可能都无法忍受。 这里的请求在.net下指代线程.
ok,那只能对这个耗时任务,进行拆解,当然前提是他可以拆解(存在并行化的可能),我们这个例子显然是可以的,于是,我这么做,先把米放到电饭煲里面,不在停留,接着立马把衣服放到洗衣机,最后再把菜放到蒸锅里.然后我去干别的事情了.代码如下:
class Program { static void Main(string[] args) { var t1=Task.Run(() => CookRice()); var t2 = Task.Run(() => CookDish()); var t3 = Task.Run(() => DoLaundry()); Task.WaitAll(t1, t2, t3); Console.WriteLine("哥们,全部搞定了"); Console.ReadKey(); } /// <summary> /// 煮饭 /// </summary> static void CookRice() { Thread.Sleep(20 * 1000 * 60); } /// <summary> /// 蒸菜 /// </summary> static void CookDish() { Thread.Sleep(10 * 1000 * 60); } /// <summary> /// 洗衣服 /// </summary> static void DoLaundry() { Thread.Sleep(25 * 1000 * 60); } }
ok,这里我们将大任务,拆分成了三个小任务,分别交给了三个线程去做.同时,我等待三个任务完成之后,告诉我哥们.这是整个任务的执行时间大大缩短,相当于原先一个人的活,交给了三个人干.能不快吗!
ok,到这里很多人觉得这样就行了.已经无法再继续优化了,这时候异步登场了.
(3)、异步优化
再优化代码之前,得知道线程池和CLR的概念,每个CLR会维护一个线程池.既然是池,说明线程的数量是有限的.并且我们的Web应用程序所使用的线程都会从CLR中去调取.那就说明,我们的Web程序能使用的线程有限.
ok.再回到上面的代码,
Task.WaitAll会阻塞主线程,主线程会在这里休眠,意味着这三个任务不做完,主线程会一直被占用.对应生活场景,就是我一直看着三台机器的执行,知道完成之后告诉我哥们.这期间我干不了任何事,只能看着.
那问题就大了.如果在高并发场景下.瞬时发起了1000条请求,那么就会产生非常多的等待线程,这些线程啥都不干,就干等着.造成了严重的资源浪费.显然是有问题的.
ok,异步登场了.
异步的原理(代码层面的介绍请百度),大致是这样,所有的线程不在等待,阻塞而是通过线程池调度,就是线程池主动通知.代码如下:
class Program { static async void Main(string[] args) { Console.WriteLine($"当前是我在工作"); var t1=Task.Run(() => CookRice()); var t2 = Task.Run(() => CookDish()); var t3 = Task.Run(() => DoLaundry()); Console.WriteLine($"我触发了await操作,就返回上一个调用方法去干别的事情去了,同时通过状态机机制(自行百度),这个方法会被暂停"); await Task.WhenAll(t1, t2, t3); Console.WriteLine("await 内部操作执行完毕,线程池委派了一个新线程来执行接下去的任务,状态机机制又会恢复当前方法,接下去执行"); Console.WriteLine("哥们,搞定了,但我不是你哥们,我是你哥们的朋友"); Console.ReadKey(); } /// <summary> /// 煮饭 /// </summary> static void CookRice() { Thread.Sleep(20 * 1000 * 60); } /// <summary> /// 蒸菜 /// </summary> static void CookDish() { Thread.Sleep(10 * 1000 * 60); } /// <summary> /// 洗衣服 /// </summary> static void DoLaundry() { Thread.Sleep(25 * 1000 * 60); } }
ok,上面的代码简要的阐述了异步的原理,通过async await编程模型,当Main方法执行到await之前时,我(主线程)就会回到上一个调用方法接着执行别的任务,如果没有返回线程池.接着通过状态机机制,暂停当前方法的执行,当await方法执行完毕时,线程池会委派新的线程回来接着执行接下去的方法,再次之前状态机会恢复方法的执行.以此类推.
通过这种方式(异步),我们的Web程序就能高效率的利用好线程.
(4)、异步在磁盘IO和网络请求上面的优势
同步程序在处理磁盘IO和网络请求时,同样会采用阻塞的方式,比如发起一个后端http、webscoket请求(使用同步方法)、文件读写请求等等,那么主线程等等到远程主机和硬件设备响应之后接着执行,期间他不会返回,会一直等.那么这和上面的问题是一样的了.这就是所谓处理IO-Bound Operation的方式,很显然,这也是一个异步操作。当我们希望进行一个异步的IO-Bound Operation时,CLR会(通过Windows API)发出一个IRP(I/O Request Packet)。当设备准备妥当,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会(通过Windows)交还一个表示工作完成的IRP。CLR会为每个进程创建一个IOCP(I/O Completion Port)并和Windows操作系统一起维护。IOCP中一旦被放入表示完成的IRP之后(通过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。这种做法的需要一个重要条件,这就是发出用于请求的IRP的操作能够立即返回,并且这个IO操作不会使用任何线程。而此时,这种异步调用是真正地在节省资源,因为我们可以腾出线程用来处理其他任务了,但是这种做法据说需要操作系统和设备的支持,但是我实际测试发现使用异步Api的收益明显要高于同步.
2、总结
综上所述,异步的优势已经非常明显了,并且Web开发,基本都是要么和tcp要么和磁盘打交道.所以用异步个人认为是最佳实践.
加载全部内容