按照咱们常规的思惟方式,计算机应该是干完一件事,而后再干下一件。用术语来讲,这种执行任务的方式叫作同步执行(Synchronous Execution)。既然这样,那么为何要引入异步执行的概念呢?
目录
为何要使用异步调用
实现异步调用的步骤和机理
为何要使用异步调用(Asynchronous Method Execution)
按照咱们常规的思惟方式,计算机应该是干完一件事,而后再干下一件。用术语来讲,这种执行任务的方式叫作同步执行(Synchronous Execution)。既然这样,那么为何要引入异步执行的概念呢?缘由很简单,由于同步执行在有些状况下效果不理想,不能完成咱们预期的目的。举两个简单的例子来讲明一下这个问题。
a. 一个客户端程序(Client Side Program)要从后台数据库取回一个复杂的数据集合。可能这个数据库操做自己很费时,也多是网络传输的数度比较慢,总之这个方法调用可能要花20秒时间。若是使用同步调用,那么在数据库结果返回以前,用户必须耐心等待,什么也不能作。这时候你可能会但愿这个调用慢慢的在别处进行,程序立刻返回好让你作其它的工做。等何时数据返回了,在进行其随后相应的操做。这种情形下,你就须要对数据库操做的方法进行异步调用。
b.一个网上机票查询订阅程序。当客户要查询从北京到芝加哥的全部机票的时候,这个程序可能要在后台经过Web Service对美国西北航空公司,中国国际航空公司和东方航空公司进行访问。将这些公司的机票状况汇总后一块儿以HTML的形式返回给用户。若是是同步调用,那么须要一个接一个进行Web Service调用。若是每一个调用花费10秒钟的话,那么整个过程就要30秒钟。若是你使用异步调用,那么你能够在同几乎一时间就对三个公司发出相应的请求,10秒后当结果从三个不一样的网站返回来后,你就能够汇总并返回各用户了。这样,整个过程只须要10秒左右。
看到这里你可能会说,这个问题没什么新鲜的。我在C++,Java里均可以用线程(Thread)来达到这样的效果。的确,大多数的高级语言都容许你建立新的线程来手工实现这样的调用。可是这些手工操做比较复杂,程序员须要本身控制线程的建立,销毁,协调等等许多细节工做,容易产生错误。而且在大型的服务器端的程序中,手工控制线程有时性能不够优化,不能根据当前具体服务器的处理器状况来动态的和智能的优化线程的数量。基于这个缘由,.NET建立了一种相对简单的异步方法调用机制,使这一问题变得更加简单。这就是今天要谈的使用表明(Delegates)对方法进行异步调用。(本文以VB.NET来进行示范,C#的异步调用和此相似,就再也不给出例程了)
实现异步调用的步骤和机理
假设有这样一个方法(Method),它接受一个班级的名称,而后查询数据库,返回这个班级全部同窗的名单。
Class DemoClass
public shared Function GetStudentsList(ClassName as String)
as String()
'查询数据库
'其它操做
End function
End Class
若是对这样一个方法进行异步调用的话,那么你首先须要定义一个有一样方法签名(Function Signature)的表明(Delegate),好比
Delegate Function GetStudentListDelegate (ClassName as String) as String()
下一步,你须要生成一个表明实例(Instance),而后将这个表明和你的真正的方法“捆绑”起来,如
Dim delegate as GetStudentListDelegate
GetStudentListDelegate = AddressOf DemoClass.GetStudentsList
(为了简单起见,这里使用了静态方法,这其实不是必须的)
当你作到这步的时候,.NET的编译器在后台为你的表明增长了几个方法,它们是Invoke, BeginInvoke, EndInvoke.
若是你使用Invoke方法,那么其效果是同步调用,好比
delegate.Invoke("class90")
在这种状况下,表明将输入参数"class90"传递给方法GetStudentsList,而后将这个方法的返回值返回给用户。这种使用方法是同步的,不是咱们所期待的。若是要达到异步效果,咱们要使用BeginInvoke和EndInvoke。
让咱们先看看BeginInvoke
你的使用方法可能以下所示:
Dim ar as System. IAsyncResult
ar = delegate.BeginInvoke("class90",Nothing, Nothing)
你可能会发现,这种调用方法有些不一样。首先是多出两个输入参数,其次是返回值是System. IAsyncResult。这究竟是怎么一回事呢?
当你调用BeginInvoke的时候,一系列的事情在后台自动发生了。
当你用表明发出调用请求后,CLR(公共语言运行环境,Common Language Runtime)接到这个请求,并将这个请求放置到一个内部的处理队列(Queue)中去。一旦放置完成后,CLR立刻就给调用者返回一个IAsyncResult的对象。这个对象很重要,咱们一下子还要解释他的具体做用。
当调用者收到返回的IAsyncResult对象后,它就能够进行下一步的工做。因为将请求放置到队列中是个很是快速的操做,因此调用者立刻就能够去完成下一个动做,没有被“阻挡(Block)”。
CLR在后台维持着一个“线程池(Thread Pool)”。这些线程守候着前面提到的那个处理队列。一旦有任务被放置到队列中,一个线程就会拿到这个任务并执行它。也就是说原来要调用者线程执行的费时的操做被线程池中的一个线程代劳了。(这里你能够看出,不论是用什么样的语言,在异步调用中,必定有其它的线程出现。或者是你手工建立它(如Java),或者是系统为你建立(如.NET)。那么这个“线程池”中究竟有几个线程呢?这个问题你能够不用关心。CLR会根据程序的特色以及当前的硬件条件自行决定。好比对于运行在单处理器平台上的通常的桌面程序,这个线程池可能有几个线程;而对于一个运行在4处理器服务器上的后台应用,线程池可能会有近百个线程。这样作的好处就是下降程序员的开发难度,让.NET的CLR去解决这些和用户应用逻辑无关的问题。)
既然有线程池的线程代替完成了那个方法调用(GetStudentsList),那么咱们怎么知道后台的这个调用何时完成呢?这个方法调用返回的值(这里是一串学生名单)咱们怎么拿到呢?这里咱们就要用到前面提到的那个返回的IASyncResult对象了。 程序员
这个IASyncResult对象一个“收据”似的,经过它你能够查询后台调用是否完成。若是已经完成,你能够经过它来取回你想要的结果。
Dim ar as System.IASyncResult
ar = delegate.BeginInvoke("class90",Nothing, Nothing)
'*** 其它一些操做
。。。
'*** 检查后台调用状态
If (ar.IsCompleted) Then
'*** 取回异步调用方法的结果
End If
若是后台调用已经结束,那么你就能够用表明的EndInvoke来获得返回值。
Dim Students as String()
Students = delegate.EndInvoke(ar)
那么,若是你没有测试后台调用是否结束而直接使用EndInvoke,那后果会怎么样呢?若是后台调用没有完成,EndInvoke调用就会被“阻挡”,直到后台调用完成后才返回。若是后台调用出现异常,那么EndInvoke还能够捕捉到这个异常
Dim Students as String()
TryStudents = delegate.EndInvoke(ar)
Catch ex as Exception
'处理这个异常
End Try
既然EndInvoke调用就会被“阻挡”(若是后台异步调用尚未完成),那么下面这种标较复杂状况CLR是怎样处理的呢?
Dim ar1, ar2 as System.IASyncResult
Dim rt1, rt2 as String()
ar1 = delegate1.BeginInvoke("class90",Nothing, Nothing)
ar2 = delegate2.BeginInvoke("class94",Nothing, Nothing)
rt1 = delegate1.EndInvoke(ar1)
rt2 = delegate2.EndInvoke(ar2)
在这个例子中,delegate1的调用和delegate2的调用完成顺序可能会有多种状况。好比delegate2的调用后发先至,那么EndInvoke的使用顺序是否是很重要呢?事实上,你能够忽略这个问题,CLR会保证在两个异步调用都结束后,你才能够进行下面的操做。至于它是怎么实现的,你能够不去管它。
事实上,EndInvoke是很是重要的。若是你使用了BeginInvoke,那你最好使用EndInvoke。由于你若是不使用EndInvoke,那么后台调用的异常就没有机会被捕捉到。另外,使用了EndInvoke可让CLR释放异步调用中所使用的资源,不然你的应用程序就可能出现资源泄漏(Resource Leak)。
到这里,状况已经比较清楚了。使用Delegate可让后台线程代替当前线程去完成费时的操做,从而使当前线程不被“阻挡”,能够立刻进行其它的工做。可是,若是当前线程经过EndInvoke来获得异步调用的结果,它又极可能被“阻挡”。看起来有点“拆了东墙补西墙”的样子,好像咱们没有获得什么好处。打个比方来讲吧,你要到复印室去复印一批材料,这个工做要费时一个多小时。同步调用就意味着你本身亲自去复印,一个多小时候再返回办公室做其它工做。异步调用意味着你能够把复印材料交到复印室,那里有专人负责复印。你放下材料后就能够回到办公室去干其它工做了。但问题是,你要不停的查看材料是否复印好了,一旦发现复印完毕后,就立刻取回做相应的操做。你不停的查看(调用表明的IsComplete方法)或者是“干等”(调用表明的EndInvoke方法)实际上仍是把你“捆住”了,你没有能腾出手来干其它的事。能不能我把材料放到复印室就无论了,等复印好后他们给我把材料送回来?。答案是能够的,那就是利用回调函数(Callback Function)。
还记得咱们前面的那个例子吗,咱们用表明调用BeginInvoke的时候,多了两个参数,其中一个就是回调函数,另一个是执行回调函数的参数。回调函数的意思是在后台线执行完异步调用的方法后,自动去执行的函数(或方法)。在执行这个回调函数的时候,你还能够指定参数。也是就说,你让复印室的复印员完成复印后,把材料给你放回到你的办公桌上,而且每10页一摞。这个“放到办公桌上”就是回调函数,而“每10页一摞”就是回调函数执行时使用的参数。
'回调函数的参数
Dim myValue as Integer = 10
'回调函数的定义
Sub PutToDesk(Byval ar as IAsyncResult)
dim x as Integer = CInt(ar.AsyncState)'拿到参数
'相应的操做
End Sub
'使用回调函数的方法
Private CallBackDelegate as AsyncCallBack = AddressOf PutToDesk
...
Dim ar as System.IASyncResult
ar = delegate.BeginInvoke("class90",CallBackDelegate, myValue)
在使用回调函数时要注意,你的回调函数必须和.NET系统定义的AsyncCallBack一块儿使用,即你的回调函数必须和AsyncCallBack具备同样的签名。也就是说它必须是子程序(Sub Procedure),必须有一个IAsyncResult类对象为输入参数。
要注意的是回调函数是由后台线程来执行的(就是咱们所说的复印员)。这种执行方法在有些状况下会形成不小的问题。好比说,在Windows的桌面应用中有这样一个规则,那就是一切用户界面元素的更改(外观以及属性)必须由这些界面元素的建立线程来进行(术语上叫界面主线程,Primary UI Thread)。若是其它线程试图更新界面元素,那么将会有不可预测的后果。若是你违反了这一原则,那么你的程序在理论上讲是不安全的,即便是问题你一时尚未发现。
就上面一个例子而言,若是后台线程从数据库里拿到了学生名单,那么极可能它要执行的回调函数就是更新界面上的一个下拉式列表(Dropdown List),或是一个表格(Grid)什么的。可是这样作又违反了咱们所说的界面更新的线程原则。那么咱们该怎么办呢?
其实这个问题并不难解决,设计师在设计.NET的时候已经考虑到了这个问题。数据库