net 异步与同步
一、摘论
为什么不是摘要呢?其实这个是我个人的想法,其实很多人在谈论异步与同步的时候都忽略了,同步异步不是软件的原理,其本身是计算机的原理及概念,这里就不过多的阐述计算机原理了。在学习同步与异步之前,我们需要先研究几个问题
在说到异步前,先来理一下几个容易混淆的概念,并行、多线程、异步。
并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中。
多线程,一般指同一进程中多个线程(包含其数据结构、上下文与代码片段)协作运行。在多核计算机中多个线程将有机会同时运行于多个核上,如果线程中进行的是计算,则行成并行计算。
异步,与同步相对应,是指呼叫另一操作后,不等待其结果,继续执行之后的操作,若之后没有其他操作,当前线程将进入睡眠状态,而CPU时间将有机会切至其他线程。在异步操作完成后通过回调函数的方式获取通知与结果。异步的实现方式有多种,如多线程与完成端口。多线程将异步操作放入另一线程中运行,通过轮询或回调方法得到完成通知;完成端口,由操作系统接管异步操作的调度,通过硬件中断,在完成时触发回调方法,此方式不需要占用额外线程。
通过上面的两张图,可以把三个概念透析的非常好理解,异步在某种意义上讲是“时空转换”即时间换空间,空间换时间。下边我们来学习下,在net 中的异步
二、同步和异步
1.同步执行
为了准备一个耗时的程序,本人准备了一本Txt修仙小说,我们用程序读取一行行输出,输出完成以后,我们输出一句话,"今天书就读到这里吧!!累了,休息一会,休息一会!一休哥",为了更好的演示同步异步,本文采用winform程序,同时为了体验winform 和控制台 带来的视觉效果,我们选择项目属性,应用程序这选择控制台。
在准备一个很费时的读书方法,
/// <summary> /// 读书,一个很废时间的任务 /// </summary> public void ReadBook() { //我们可以通过 Thread.CurrentThread.ManagedThreadId 获取当前线程的唯一标识符 Console.WriteLine("********************** ReadBook Start【" + Thread.CurrentThread.ManagedThreadId + "】等待............... **********************************************"); System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start(); string Path= AppDomain.CurrentDomain.BaseDirectory + "zqjz.txt"; List<string> list = new List<string>(); System.IO.StreamReader sr = new System.IO.StreamReader(Path, Encoding.Default); string line = ""; Console.ForegroundColor = ConsoleColor.Black; while ((line = sr.ReadLine()) != null&& list.Count<120) { char[] array= line.ToArray(); for (int i = 0; i < array.Length; i++) { Console.Write(array[i]); if (i!=0) { // Thread.Sleep(128);//人眼最快敏感视觉是128毫秒左右,我们这里测试先使用10毫秒 Thread.Sleep(10); } } Console.WriteLine(); Console.BackgroundColor = (ConsoleColor)new Random().Next(3, 15); list.Add(line); } sr.Close(); sr.Dispose(); watch.Stop(); Console.WriteLine("今天读书用了"+ watch.ElapsedMilliseconds+"豪秒"); Console.WriteLine("********************** ReadBook End【" + Thread.CurrentThread.ManagedThreadId + " 】**********************************************"); }
这个方法比较简单,就是读取电子书,同时给方法加上了耗时记录,和当前线程的唯一标识。现在我们在窗体上加上一个buttion 调用下我们的读书。看看结果是怎么样的,同时建议打开任务管理器,监控下CPU,等cpu 平稳以后,我们在点击同步执行按钮。“现在是我们自己在读书”。
2.异步执行
关于异步在前边的摘论里面介绍了大概,这里不过多演示,请继续看!在早期,net 的异步都是在使用委托来做的,而委托使用的是线程池ThreadPool来实现的,曾取下一篇文章介绍线程,到时候在详细介绍线程池,关于委托请观看本人前边的文章 "linq to Objet",我们在程序上在加上一个按钮,里面老师读书,我的心缺飞了,在想下课玩什么?怎么和同学玩。
private void btnSync_Click(object sender, EventArgs e) {//同步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); ReadBook(); MessageBox.Show("今天书就读到这里吧!!累了,休息一会,休息一会!一休哥"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); } private void btnasync_Click(object sender, EventArgs e) {//异步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); Action action = new Action(() => ReadBook()); action.BeginInvoke(null,null);//参数先不管,我们先给null,一会我们会继续演示 MessageBox.Show("今天想玩,怎么骗过老师呢!!书还在继续读,但是我已经在玩了!!!"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); }
上面代码分别为异步调用和同步调用,下图为异步调用结果,我们会发现,异步调用窗体是可以移动的,并且CPU 会有很大的波峰,细心的人会发现,执行时间是一样的,只是他们的线程唯一标识是不一样的。
通过上述演示,异步和同步的区别很简单了吧!这里就不过多描述,自己总结。但是我们的要说下异步和多线程的区别?其实异步只是一种结果(目地),而多线程才是实现这种结果的一种方式,在NET 里面,异步和多线程没有本质的区别,个人总结唯一的区别就是,应用场景不同。
重点:多播委托不可以指定异步。不予显示,自己去尝试和找寻原理,实在找不到原理可以理解为这是任何高级语言的一个规定。有关多播委托请参考本人:一步一步带你了解 Linq to Object
三、异步回掉和异步等待(阻塞)
1.异步回掉:
刚才我们一直在上课读书,但是我的心里在想的是下课去哪里玩,如何玩?这个时候,我们需要在异步读书的方法之后也就是下课以后再去玩。看下代码是怎么写的。
//异步 Console.WriteLine("**********************btnSync_Click Start【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************"); #region 异步回调 IAsyncResult iAsyncResult = null; AsyncCallback callback = t => { Console.WriteLine(t); Console.WriteLine("下边代码是比较两个对象是否一样"); Console.WriteLine($"string.ReferenceEquals(t, iAsyncResult)={string.ReferenceEquals(t, iAsyncResult)}"); Console.WriteLine($"当前线程ID {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"终于下课了!我们走吧,尽情的玩吧,你问我老师讲的啥,我知道!!"); };//AsyncCallback 本身就是一个委托,这个委托有一个参数,这个参数就是我们委托的BeginInvoke的返回值。我们使用这个委托去做异步回调 #endregion Action action = () => ReadBook();//简写 iAsyncResult= action.BeginInvoke(callback, null);//这里的第一个参数,我们就是异步回调 MessageBox.Show("今天想玩,怎么骗过老师呢,下课玩点什么呢!!书还在继续读,但是我的心已经飞了!!!"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
所谓的异步回调,就是在异步线程执行完在执行的代码块。
2.异步等待(阻塞):
主线程等待子线程有这么几种方式:
1.主线程等待子线程的时候有返回,比如说我们常见的进度条功能,执行一点我就返回下。2.主线程等待子线程有时间限制,例如:中午放学我等你五分钟,你要是不完事,我就先吃饭去了。3.主线程等待子线程无返回,比如死等,今天的代码我学不会了,我就不睡觉了。下面我们分别看看这三种情况。我们管操作线程等待的过程叫做阻塞(se)进程.阻塞主进程以后等待子线程的执行,我们成为线程的阻塞,
刚才我们是使用回调,在异步执行完成,才执行了一个代码块,这个时候messagebax 已经输出了,现在我们开看看课堂下的学生表现。“将下列代码放到我们 MessageBox.Show("今天想玩,怎么骗过老师呢,下课玩点什么呢!!书还在继续读,但是我的心已经飞了!!!");”之后,我们来看看
while (!iAsyncResult.IsCompleted)//边等待边操作,可以用于做进度条 { Thread.Sleep(100);//建议控制100毫秒一次 Console.WriteLine("老师还在教我们读书.....请等待..............."); } //当异步完成以后,我们在执行下边的这句话 Console.WriteLine("学生甲:冲啊..............打篮球全"); Console.WriteLine("学生乙:王美美.......我爱你!咱们交往吧....*#*#*#**??!"); Console.WriteLine("学生丙:呼呼呼呼呼呼呼呼。。。。。噜。。。。。。。。。。今天的肉丸子真好吃,真希望这不是梦啊"); Console.WriteLine("学生丁:大海啊,就像妈妈一样,海浪啊!你为啥这么猛!总是在我人生巅峰......被打断"); Console.WriteLine("学生丙:别BiBi了,海浪是你后妈,滚一边去淫诗去!别TMD打扰老子睡觉");
刚才执行的线程等待在阻塞的过程中是有损耗的,我们损耗 的是时间,所以回调会在子线程之前执行,那么我们想要无损耗怎么去写,怎么去阻塞我们的主线程呢 “ bool RunBool = iAsyncResult.AsyncWaitHandle.WaitOne();”; 当子线程执行成功了,就会返回TRUE,当子线程执行过程中出现exection 以后,就返回false;
这种写法主线程就无法返回了。但是我们可以新建立一个线程去监控子线程。这里就不写那么复杂了。
第二种情况,我只等你两秒钟,有时间限制的阻塞
#region 异步等待1 有损耗 带返回 //while (!iAsyncResult.IsCompleted)//边等待边操作,可以用于做进度条 //{ // Thread.Sleep(100);//建议控制100毫秒一次 // Console.WriteLine("老师还在教我们读书.....请等待..............."); //} #endregion #region 异步等待2 无损耗 无返回 //bool RunBool = iAsyncResult.AsyncWaitHandle.WaitOne();//返回结果是子线程执行成功或者失败,不是实时返回的。 //iAsyncResult.AsyncWaitHandle.WaitOne(-1);//写法2 #endregion #region 有时间限制的异步等待 iAsyncResult.AsyncWaitHandle.WaitOne(2000);//我最多等你2秒钟,如果你提前完事,我们提前走 #endregion //当异步完成以后,我们在执行下边的这句话 Console.WriteLine("学生甲:冲啊..............打篮球全"); Console.WriteLine("学生乙:王美美.......我爱你!咱们交往吧....*#*#*#**??!"); Console.WriteLine("学生丙:呼呼呼呼呼呼呼呼。。。。。噜。。。。。。。。。。今天的肉丸子真好吃,真希望这不是梦啊"); Console.WriteLine("学生丁:大海啊,就像妈妈一样,海浪啊!你为啥这么猛!总是在我人生巅峰......被打断"); Console.WriteLine("学生丙:别BiBi了,海浪是你后妈,滚一边去淫诗去!别TMD打扰老子睡觉"); Console.WriteLine("**********************btnSync_Click End 【" + Thread.CurrentThread.ManagedThreadId + "】**********************************************");
这种子线程执行两秒以后,主线程在执行这个问题经常会在面试里面问。面试经常会问,主线程A 至少要执行10,秒,子线程B至少要执行30秒,如何让主线程在子线程执行20秒开始执行。
下边我们就举例,代码不会,我就要学习了学习不会就不睡觉,就死学到底了。
#region 异步等待死等 //死等就是,只要你不异常,就必须给我一个结果,比如学习,必须学会为止 action.EndInvoke(iAsyncResult);//EndInvoke 的返回值取决与你的委托!你的委托有返回值,我就有返回值。 #endregion
注意图上反应的问题。其实回调执行的是子线程。我们死等(阻塞 主线程等待子线程)的是子线程,而不是子线程的回调。这个时候是主线程和子线程一起执行的(线程的无序)。这就会照成CPU 更大的波峰,很容易宕机。由于演示这种结果不容易,需要执行很多遍,这里没有截取到CPU 波峰。本人I7 CPU 基本都赶到顶了。
通过上图可以看出,主线程和子线程的执行先后顺序不一定谁先后,线程是无序的。
如果下了本文demo 的同学会发现,这个时候UI 是卡住的,主窗体UI阻塞,所以窗体是无法移动的。
。到这里异步我们就学习完了,下边总结下
四、总结
1.异步等待和异步回调的区别?面试会考的哦!!
答:异步等待是在子线程执行完成以后回到主线程,j解除主线程的阻塞继续执行,而异步回调是子线程执行完成以后在去以子线程再去执行的任务代码块。
异步等待卡主线程,回调不卡主线程。
在委托中回调不可以取得子线程执行的结果,等待可以通过线程状态参数取得执行结果。
2.主线程A 需要执行1秒,而子线程B需要执行3秒。如果让B执行2秒以后在执行?或者 接口A 调用5秒没结果,我就调用接口B去取数据?在接口B取到数据以后,接口如果也取到数据,仍然使用结果B的,怎么去做。
答:使用 iAsyncResult.AsyncWaitHandle.WaitOne(2000);
关于接口(WebApi ,Service)的情况,我们也是需要使用线程等待,但是这个时候我们就要加锁或者加计时器 StopWatch 去做。关于锁以后在谈。但是加锁会影响效率,计时器在多服务情况下还不准确,这是大多数面试者的回答。
我们把没有演示的一点点知识在这里演示下。
我们一直没有说这个参数有什么做用,这里简单介绍下。当我线程启动的时候,我可以启动多条线程,但是我无法确定那个线程执行的过程,这个时候我们可以通过这个参数传递线程状态。这里不过多解释。有用到的私聊本人。
3.如果我想使用子线程的结果去做主线程的参数,如何去做。请说明你的理由。这里不过多解释了,案列很清晰。
4.这里的阻塞是卡主线程的,我们如何不卡主线程??
下节多线程中找答案。
个人总结:
1.net 异步支持
Net framework可以让你异步调用任何方法。为达这样的目的,你可以定义一个与你要调用的方法的签名相同的委托。公共语言运行时将自动为该委托定义与签名相同的BeginInvok和EndInvoke方法。
异步委托调用BeginInvok和EndInvoke方法,但在.NET Compact Framework中并不支持。
.NET Framework 允许您异步调用任何方法。定义与您需要调用的方法具有相同签名的委托;公共语言运行库将自动为该委托定义具有适当签名
的 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法用于启动异步调用。它与您需要异步执行的方法具有相同的参数,只不过还有两个额外的参数(将在稍后描述)。
BeginInvoke 立即返回,不等待异步调用完成。
BeginInvoke 返回 IasyncResult,可用于监视调用进度。
EndInvoke 方法用于检索异步调用结果。调用 BeginInvoke 后可随时调用 EndInvoke 方法;如果异步调用未完成,EndInvoke 将一直阻塞到
异步调用完成。EndInvoke 的参数包括您需要异步执行的方法的 out 和 ref 参数(在 Visual Basic 中为 <Out> ByRef 和 ByRef)以及由
BeginInvoke 返回的 IAsyncResult。
四种使用 BeginInvoke 和 EndInvoke 进行异步调用的常用方法。调用了 BeginInvoke 后,可以:
1.进行某些操作,然后调用 EndInvoke 一直阻塞到调用完成。
2.使用 IAsyncResult.AsyncWaitHandle 获取 WaitHandle,使用它的 WaitOne 方法将执行一直阻塞到发出 WaitHandle 信号,然后调用
EndInvoke。这里主要是主程序等待异步方法,等待异步方法的结果。
3.轮询由 BeginInvoke 返回的 IAsyncResult,IAsyncResult.IsCompeted确定异步调用何时完成,然后调用 EndInvoke。此处理个人认为与
相同。
4.将用于回调方法的委托传递给 BeginInvoke。该方法在异步调用完成后在 ThreadPool 线程上执行,它可以调用 EndInvoke。这是在强制装
换回调函数里面IAsyncResult.AsyncState(BeginInvoke方法的最后一个参数)成委托,然后用委托执行EndInvoke。
警告 始终在异步调用完成后调用 EndInvoke。
通过EndInvoke方法检测异步调用的结果。如果异步调用尚未完成,EndInvoke将阻塞调用线程,直到它完成。EndInvoke参数包括out和ref参数,本文没有讲到,另外本文没有演示EndInvoke 返回值 。
2.同步方法和异步方法的区别
同步方法调用在程序继续执行之前需要等待同步方法执行完毕返回结果
异步方法则在被调用之后立即返回以便程序在被调用方法完成其任务的同时执行其它操作
Demo 下载