声明: 今天遇到异步线程处理问题 网上搜到的文章
最近我编写了很多智能客户端应用,总结了一些能够使应用程序在后台调用Web Service时不冻结前台界面的异步调用方法。虽然当前.NET Framework本身已经提供了异步调用的机制,但我发现在Windows应用中这一机制比较难于把握,因为这时你需要正确的控制用户界面线程处理。
在这篇文章中,我将教给您一种在Windows应用程序中实现异步调用Web服务的简单方法,通过这一方法,您不用再考虑后台线程与前台界面线程的交互关系了。
服务代理
Visual Studio? .NET会生成较好的Web服务代理类,通过它可以异步的使用Web服务,但是这个代理类实现的是.NET Framework本身的异步调用机制,如上所述,这一机制对于Windows应用来说并不十分方便。由于这个原因,我一般不直接使用生成的代理类,而是在中间增加服务代理类。
服务代理类就是增加了额外功能的类,这些功能可以帮助客户端程序与Web服务进行交互。服务代理类实现了许多有用的功能,包括数据缓存,安全身份管理,离线操作支持等等。本文中创建的服务代理类比.NET Framework本身的普通代理类实现了更简便的异步调用模式。
用户界面线程
应用程序从一个创建和管理用户界面的线程起始,这一线程被称为用户界面线程。大多数开发者本能的会使用用户界面线程完成所有的工作,包括进行Web服务调用,远程对象调用,访问数据库等等,大多数使用和性能方面的问题是由这一不恰当的方法引起的。
问题的本质是你永远不可能精确的预知访问Web服务,远程对象,或者数据库所需的时间。而且当你在用户界面线程中进行这类的调用时,用户界面就有可能会产生令人恼怒的冻结。
自然而然的,你会把这一类的调用放置在一个单独的线程中,但我更进了一步,建议您把所有的非用户界面工作坊制在一个分离的线程中。我的观点是,用户界面线程只用来管理用户界面,而所有那些你不能保证良好响应时间的对象调用都应该是异步的,无论是进程内的,跨进程的,还是跨计算机的。
无论如何,尽量使用户界面线程处理的异步调用模式简单化,我已经实现了一个与Visual Studio 2005里某个特性类似的简单异步调用模式。作为开始,我们首先解释一下当前.NET Framework中异步调用模式的工作原理。
.NET异步调用模式
系统生成的Web服务代理类的每个Web函数都有一个Begin和一个End方法,每个支持.NET Framework异步调用模式的对象都和这个类似。开始进行异步调用时,客户端调用Begin方法时就立即响应,或者在建立了访问Web服务的独立线程后马上响应。在这之后的某个时间,当Web服务访问完成后,客户端再调用End方法。
但客户端如何知道什么时候调用End方法呢?Begin方法会返回一个IAsyncResult对象,可以帮助你跟踪异步调用的过程,也可以明确的等待后台线程完成,但如果在用户界面线程中进行这些工作,会降低整个系统的同步性。更好的方法是,在用户界面进程中注册一个回调函数,当其它工作完成时产生一个自动通知。
让我们看一段样例代码,在这段代码中,我们从一个Web服务中获取一些客户数据,这些功能通过Web服务代理类里的GetCustomerData方法完成。我们可以启动这个Web服务调用,并且用以下代码注册一个回调函数,用来在用户界面线程中产生与应用程序进行交互的功能。
private void SomeUIEvent( object sender, EventArgs e )
{
// Create a callback delegate so we will
// be notified when the call has completed.
AsyncCallback callBack = new
AsyncCallback( CustomerDataCallback );
// Start retrieving the customer data.
_proxy.BeginGetCustomerData( "Joe Bloggs", callBack, null );
}
Web服务调用最终返回CustomerDataCallback方法,在这个方法中,我们需要调用真正用于获取客户数据的Web服务代理类中的End方法,这个方法可以实现如下:
public void CustomerDataCallback( IAsyncResult ar )
{
// Retrieve the customer data.
_customerData = _proxy.EndGetCustomerData( ar );
}
现在,你必须注意,这一方法是被后台工作线程调用的。如果想在前台界面上使用刚刚获得的信息(例如在一个data grid控件中显示那些客户数据),一定不要忘记在用户界面线程中进行更新。如果忘了这样做,就会发生很多莫名其妙的错误,而且调试起来还相当的不易。
那么我们怎么切换线程呢?我们可以使用服务代理的方法,所有的控件都源自这些对象的实现。我们可以实现一个从用户界面线程调用的方法,在这个方法内我们可以安全的更新我们的界面。使用Control.Invoke方法,我们必须给用户更新方法传递一个委托,现在,CustomerDataCallback方法的代码更新如下:
public void CustomerDataCallback( IAsyncResult ar )
{
// Retrieve the customer data.
_customerData = _proxy.EndGetCustomerData( ar );
// Create an EventHandler delegate.
EventHandler updateUI = new EventHandler( UpdateUI );
// Invoke the delegate on the UI thread.
this.Invoke( updateUI, new object[] { null, null } );
}
UpdateUI方法可以按如下办法实现:
private void UpdateUI( object sender, EventArgs e )
{
// Update the user interface.
_dataGrid.DataSource = _customerData;
}
这并不是十分非常严谨科学,因此可以使这一“两次跳转”的复杂问题简单化起来。关键在于异步方法的原始调用(以WinForm类为例)用来负责转换线程,并且需要另一个委托以及Control.Invoke方法。
返回页首
一个简化的异步调用模式
我经常使用一项技术来减少创建异步调用时的复杂度和代码量,那就是把线程切换和委托的实现放入一个中间类中。这就使得我们从用户界面类中进行异步调用时,不用再去考虑什么线程和委托的问题。我把这项技术叫做自动回调。使用这项技术,上面的样例可以进行如下改进:
private void SomeUIEvent( object sender, EventArgs e )
{
// Start retrieving the customer data.
_serviceAgent.BeginGetCustomerData( "Joe Bloggs" );
}
当Web服务访问完成后,以下的方法就会自动被调用:
private void GetCustomerDataCompleted( DataSet customerData )
{
// This method will be called on the UI thread.
// Update the user interface.
_dataGrid.DataSource = customerData;
}
回调函数的名称由原始异步调用的名称来决定(因此就不再需要创建和传递委托了),并且可以保证被正确的线程所调用(也就不再需要使用Control.Invoke了),这些方法都很简单并且不容易出错。
天下没有免费的午餐,实现这个简单模型的神奇代码是需要我们来编写的。下面所列的就是被编写进ServiceAgent类中的这些代码:
public class ServiceAgent : AutoCallbackServiceAgent
{
private CustomerWebService _proxy;
// Declare a delegate to describe the autocallback
// method signature.
private delegate void
GetCustomerDataCompletedCallback( DataSet customerData );
public ServiceAgent( object callbackTarget )
: base( callbackTarget )
{
// Create the Web service proxy object.
_proxy = new CustomerWebService();
}
public void BeginGetCustomerData( string customerId )
{
_proxy.BeginGetCustomerData( customerId,
new AsyncCallback( GetCustomersCallback ), null );
}
private void GetCustomerDataCallback( IAsyncResult ar )
{
DataSet customerData = _proxy.EndGetCustomerData( ar );
InvokeAutoCallback( "GetCustomerDataCompleted",
new object[] { customerData },
typeof( GetCustomersCompletedCallback ) );
}
}
这个样例中服务代理类的代码是简单容易的,而且完全可以重用,我们所要做的就是给WinForm类编写一套类似的通用代码。我们有效的提升了线程管理工作的重要性,并且把它同编写后台异步调用对象代码的工作以及编写前台客户端代码的工作分离了开来。
AutoCallbackServiceAgent基类是一个实现了InvokeAutoCallback方法的简单类,代码如下:
public class AutoCallbackServiceAgent
{
private object _callbackTarget;
public AutoCallbackServiceAgent( object callbackTarget )
{
// Store reference to the callback target object.
_ callbackTarget = callbackTarget;
}
protected void InvokeAutoCallback( string methodName,
object[] parameters,
Type delegateType )
{
// Create a delegate of the correct type.
Delegate autoCallback =
Delegate.CreateDelegate( delegateType,
_callbackTarget,
methodName );
// If the target is a control, make sure we
// invoke it on the correct thread.
Control targetCtrl = _callbackTarget as
System.Windows.Forms.Control;
if ( targetCtrl != null && targetCtrl.InvokeRequired )
{
// Invoke the method from the UI thread.
targetCtrl.Invoke( autoCallback, parameters );
}
else
{
// Invoke the method from this thread.
autoCallback.DynamicInvoke( parameters );
}
}
}
以上这些代码创建了一个回调函数的委托,并且判断是在调用线程,还是在用户界面线程中调用它。如果调用的目标是一个控件对象,那么它就会在需要的时候从用户界面线程来调用回调函数。
探究这些有趣的细节,如果你仔细的查看代码,你会发现我们可以通过不在基类中指定自动回调委托来进行简化。如果我们不需要对回调委托进行签名,我们就可以几乎自动化的处理所有的事情,把基础的服务代理类简化成只在BeginGetCustomerData方法中实现一行代码。
那我们为什么还要指定这个委托呢?那是因为我们还需要使用Control.Invoke方法。不幸的是.NET Framework的开发者并没有为这一方法提供一个MethodInfo对象,而恰恰是它可以使编写基础代码的工作变得简单许多。
一个替代的办法是指定一个标准的委托类型,把它用于所有的回调函数签名。举例来说,我们可以要求所有的自动回调函数都使用一个方法签名,这个方法签名用来维护原始的对象组,并且向客户端回传Web服务的参数。委托的声明方法如下:
public delegate void AutoCallback( object[] parameters );
使用这个委托我们可以极大地简化服务代理类的代码,但是必须在客户端代码中把返回的数据转换成一定的格式。