UML软件工程组织

.NET Framework 精简版后台处理技术

摘要:执行后台处理牵涉到很多方面,要求非常仔细地进行设计。本文提供了一些有关充分利用后台处理的建议,并提出了很多必须解决的问题。

简介

应用程序通常需要在后台执行长时间运行的操作,同时还要提供积极的、响应及时的用户体验。这种情况在智能设备开发过程中最为常见。Pocket PC 上运行的应用程序必须能够随时执行用户所需的任务,但是,由于此类设备的 CPU 处理能力通常都有限,而且常常使用低带宽连接,因此限制了很多进程的执行速度,对较为复杂的应用程序更是如此。通常情况下,要处理长时间运行的任务,最好是在后台进行长时间处理,同时使用户可以与应用程序进行交互。

执行后台处理牵涉到很多方面,要求非常仔细地进行设计。下面提供了一些有关充分利用后台处理的建议,并提出了很多必须解决的问题。

后台处理的类型

.NET Framework 精简版提供了三种基本技术,支持将长时间运行的处理移到后台进行。异步 XML Web Service、线程池和显式创建的线程这三种技术分别适用于不同的情况,需要正确地应用和管理。

异步 XML Web Service

Web 服务为智能设备与中央服务之间的通信提供了一种易于使用的机制。通常情况下,Web 服务必须传输大量数据,而且必须通过低速移动连接来实现传输。此类长时间运行的数据传输最适合利用 .NET Framework 精简版的功能来异步管理 Web 服务调用。

通常情况下,Web 服务是通过 Visual Studio .NET 的“Add Web Reference”(添加 Web 引用)或 WSDL 命令行实用程序生成的代理类调用的。利用此技术,应用程序开发人员可以对代理类调用本地方法,控制实际调用远程 Web 服务所需的 XML 的生成、传输和分析等方面的细节。生成的代理类为每个 Web 服务操作提供三个方法。其中一个方法的名称与异步调用 Web 服务的 Web 服务操作相同,另外两个方法的前缀分别为“Begin”和“End”,用于异步调用 Web 服务。例如,执行 Add(添加)操作的 Web 服务代理包含名为 Add、BeginAdd 和 EndAdd 的方法。

注意:有关使用 Visual Studio .NET 的“Add Web Reference”(添加 Web 引用)功能的详细信息,请参阅 Adding and Removing Web References(英文)。有关 WSDL 命令行实用程序的详细信息,请参阅 Web Services Description Language Tool(英文)。

进行异步 Web 服务调用

异步启动 Web 服务与调用该操作的“Begin”方法一样简单。假设 Web 服务 Add(添加)操作需要两个整数参数。以下代码将异步调用 Web 服务。

MathProxy wsMath = new MathProxy();
wsMath.BeginAdd(10, 15, null, null);

调用 BeginAdd 并不会直接调用 Web 服务,而是对实际的 Web 服务调用进行排队,然后立即返回。调用返回时,Web 服务本身不提供任何信息,而是返回一个 IAsyncResult 引用,用于检索实际 Web 服务调用的结果。

class MyCalcForm : Form
{
private IAsyncResult _addAsyncResult ;
private MathProxy _wsMath = new MathProxy() ;
public void btnAdd_OnClick(object sender, EventArgs e)
{
_addAsyncResult = _wsMath.BeginAdd(10, 15, null, null) ;
}

IAsyncResult 使应用程序能够确定调用完成的时间,并且可以检索调用结果。将 IAsyncResult 存储为类成员后,应用程序即可调用与启动时不同的方法,检索 Web 服务的调用结果。

为了检索 Web 服务的结果,应用程序调用代理的“End”方法,传递前面返回的 IAsyncResult,如以下代码所示。

int sum = _wsMath.EndAdd(_addAsyncResult) ;

“End”方法返回 Web 服务的返回值以及任何 ref (ByRef) 或 out 参数。

确定异步 Web 服务调用完成的时间

应用程序必须先确定调用已经完成,然后才能检索异步 Web 服务调用的结果。应用程序可以选择完成时通知应用程序、中断等待完成或轮询三种方式确定 Web 服务是否完成。

通知

大多数情况下,通知是最佳选择,因为它允许应用程序启动对 Web 服务的调用,然后不需要任何特殊处理即可继续进行。Web 服务启动后,应用程序向“Begin”方法传递一个 AsyncCallback 委托。Web 服务调用完成后,将自动调用该委托。使用通知时,不需要存储“Begin”方法返回的 IAsyncResult,因为它将作为参数自动传递给通知委托。

class MyCalcForm : Form
{
private MathProxy _wsMath = new MathProxy();
public int _sum;
public void btnAdd_OnClick(object sender, EventArgs e)
{
AsyncCallback cb = new AsyncCallback(OnAddComplete);
_wsMath.BeginAdd(10, 15, cb, null); // 初始化 XML Web Service 调用
}
public OnAddComplete(IAsyncResult ar) // 完成后由框架调用
{
_sum = _wsMath.EndAdd(ar); // 获取 XML Web Service 结果
}

警告:委托调用的方法在后台线程而不是主应用程序线程中运行。因此,委托方法无法安全地影响用户界面。有关详细信息,请参阅本文后面的“后台处理和用户体验”一节。

中断

有些情况下,应用程序在返回 Web 服务结果之前,只能执行一部分工作。在这种情况下,最好是先启动 Web 服务,执行工作中不依赖于 Web 服务结果的部分,然后中断,等待 Web 服务完成。IAsyncResult.AsyncWaitHandle 即提供了此功能。Web 服务调用启动后,AsyncWaitHandle 处于未收到信号状态,当 Web 服务调用完成后,AsyncWaitHandle 将收到信号。使用 AsyncWaitHandle 中的 WaitOne 方法会导致调用线程(通常为主应用程序线程)中断,直到 Web 服务完成。

public void DoCalculations()
{
IAsyncResult ar = _wsMath.BeginAdd(10, 15, null, null);
// 执行不需要 Add(添加)操作结果的工作
ar.AsyncWaitHandle.WaitOne() ; // 中断,直到调用完成
int sum = _wsMath.EndAdd(ar) ; // 检索结果
// 继续处理
}

轮询

少数情况下,应用程序可能希望一边进行处理,一边周期性地检查 Web 服务调用。使用 IAsyncResult.IsCompleted,应用程序可以确定 Web 服务调用是否完成。

IAsyncResult ar = _wsMath.BeginAdd(10, 15, null, null);
While (! ar.IsCompleted) // 循环,直到调用完成
{
// 等待时执行其他工作
}
int sum = _wsMath.EndAdd(ar) ; // 检索结果

线程池

当应用程序需要执行长时间运行的本地进程(例如很长的计算、文件处理或初始化)时,可以利用 .NET Framework 精简版提供的内置线程池。线程池允许在后台运行多个工作,而不需要为每个任务频繁地创建和销毁单独的线程,从而减少了开销。

.NET Framework 精简版线程池通过 ThreadPool 类提供。通过将一个方法打包到 WaitCallback 委托中,然后将该委托传递给 ThreadPool.QueueUserWorkItem 静态方法,在线程池中对任务进行排队。

void ReadBigFile(object val)
{
// 执行读取和处理文件的操作
}

public void btnStartRead_Click(object sender, EventArgs e)
{
// 将 ReadBigFile 打包到委托中并提交给线程池
WaitCallback w = new WaitCallback(ReadBigFile) ;
ThreadPool.QueueUserWorkItem(w) ;
}

在上面的示例中,ReadBigFile 方法将在线程池中排队并在一个可用的线程上运行。对 QueueUserWorkItem 的调用将中断,直到将请求放入队列中。

当要向后台进程传递参数时,QueueUserWorkItem 将提供一个重载,该重载接受一个附加参数,然后将其传递给已排队的方法。该参数被定义为对象,因而可以传递任何类型的参数。

void ReadBigFile2(object val)
{
string dataFile = (string) val ; // val 是对 fName 的引用
// 执行读取和处理 dataFile 的操作
}

public void btnStartRead_Click(object sender, EventArgs e)
{
string fName = "BigDataFile.xml" ;
WaitCallback w = new WaitCallback(ReadBigFile2) ;
// fName 将被传递给 ReadBigFile
ThreadPool.QueueUserWorkItem(w, fName) ;
}

显式创建的线程

有些情况下,后台进程(例如连续读取全球定位系统或监视设备)会占用应用程序的大部分运行时间。在这些情况下,最好为特定的任务创建专用的线程。

专用线程通过 Thread 类的实例实现,并通过将方法包装到 ThreadStart 委托,将此委托传递给 Thread 类构造函数,然后调用 Thread.Start 方法来创建。

void ReadGPSFeed()
{
// 循环读取 GPS 数据
}

public void btnStartGPS_Click(object sender, EventArgs e)
{
ThreadStart startMethod = new ThreadStart(ReadGPSFeed);
Thread gpsThread = new Thread(startMethod);
gpsThread.Start(); // 启动后台线程
}

在本例中,ReadGPSFeed 方法运行在一个新创建的线程上。此线程专用于 ReadGPSFeed 方法,并在该方法退出后终止。

通过专用线程完成的工作通常是以某种循环的方式执行处理。尽管在有些情况下由后台进程本身确定处理是否完成,但更多的情况下是由某个外部事件(如用户请求或应用程序关闭)来确定。以信号方式通知后台线程终止的最简单方式是通过类级别的布尔标志,如以下代码所示:

Class MyForm : Form
{
// 为清楚起见,省略了部分类成员
private bool _continueGPSRead = false; // 读取控制标志
void ReadGPSFeed()
{
while (_continueGPSRead) // 循环,直到为 false
{
// 读取 GPS 数据
}
}
public void btnStartGPS_Click(object sender, EventArgs e)
{
ThreadStart startMethod = new ThreadStart(ReadGPSFeed);
Thread gpsThread = new Thread(startMethod);
_continueGPSRead = true; // 设置循环标志
gpsThread.Start(); // 启动后台线程
}
public void btnStopGPS_Click(object sender, EventArgs e)
{
_continueGPSRead = false ; // 发送 ReadGPSFeed 信号以终止
}
}

在本例中,btnStartGPS_Click 通过将标志设置为“true”并启动相关线程,以启动后台处理。ReadGPSFeed 方法继续处理,直到通过 btnStopGPS_Click 将 _continueGPSRead 标志设置为“false”为止。

后台处理和用户体验

后台处理是一个功能强大的工具,正确使用可以显著改善用户体验。但是,如果使用不当,也容易产生混乱甚至引起错误。要获得良好的用户体验,应记住以下几个要点。 不要直接影响用户界面一个应用程序最初只启动一个线程。正常情况下,所有用户界面控件均由该线程创建。Windows CE 用户界面对象具有“线程关系”,这意味着这些界面对象与创建它们的线程紧密耦合在一起。这种紧密耦合是由于所有与 Windows CE 用户界面对象的交互(读取和更新)均依赖于创建线程所管理的消息队列。与创建线程相比,与线程中的界面对象的消息队列进行交互容易引起数据损坏或其他错误。.NET Framework 精简版中的 Windows 窗体控件是 Windows CE 用户界面对象上的简便包装,它也具有同样的限制。此限制同时适用于线程池和显式创建的线程。

要允许后台进程与用户界面进行安全的交互,所有 Windows 窗体控件都提供了一个特殊的 Invoke 方法。调用 Invoke 方法可以将控件传递给创建 Windows 窗体控件的线程,并在此线程上执行一个委托。用户界面线程中运行的方法必须包装在一个事件处理程序委托中。

class MyForm : Form
{
// 为清楚起见,省略了一些成员
private MathProxy _wsMath = new MathProxy();
protected TextBox _txtSum; // 将显示 XML Web Service 的结果
protected int _sum ;
public void btnAdd_OnClick(object sender, EventArgs e)
{
AsyncCallback cb = new AsyncCallback(OnAddComplete);
_wsMath.BeginAdd(10, 15, cb, null); // 启动 XML Web Service 调用
}
public OnAddComplete(IAsyncResult ar) // 完成后由框架调用
{
_sum = _wsMath.EndAdd(ar); // 获取 XML Web Service 结果
// 在创建 _txtSum 的线程上执行 UpdateSumDisplay
_txtSum.Invoke(new EventHandler(UpdateSumDisplay));
}
public void UpdateSumDisplay(object sender, EventArgs e)
{
_txtSum.Text = _sum.ToString();
}

在本例中,btnAdd_OnClick 进行异步 Web 服务调用。异步调用完成后,将自动调用 OnAddComplete 方法。正如“异步 Web 服务”一节所述,OnAddComplete 方法在后台线程中运行,因此无法直接、安全地影响 _txtSum 文本框。使用 Invoke 方法,可以将控件传递给创建 _txtSum 的线程。然后,在此线程中运行 UpdateSumDisplay。

注意:Invoke 是一个中断调用。Invoke 线程将中断,直到从被调用的方法返回。

限制后台任务的并发数量

执行的每个线程都会消耗有用的设备资源,并增加应用程序的复杂性。启动大量并发后台任务可能导致应用程序速度缓慢或响应迟钝。仅当任务在前台运行时会导致长时间或令人不舒服的延迟时,才应该在后台运行该任务。

提供可视提示

后台任务运行在线程上,而不是运行在用户界面上,因此不提供后台处理状态的自动确认。应用程序必须采取特定的步骤来通知用户。大多数情况下,最好在用户的窗体中显示一个状态字段。这有助于用户了解后台进程的运行状态。

指示启动

大多数情况下,后台处理是在响应用户操作(例如单击按钮操作)时启动的。在这种情况下,应在处理完成之前禁用启动后台进程的控件。禁用控件既可以确认后台进程已启动,又可以防止用户不小心启动任务的其他实例。还要禁用并清除后台任务完成后要更新的任何控件。

指示完成

完成后,应用程序应在不干涉用户执行其他操作的前提下提供清楚的指示。处理结果应便于使用,并应启用任务开始时禁用的控件。如果遇到长时间运行的计算(后台处理产生几个简单的值),应将结果直接显示在启动该处理的窗体中。如果结果过于复杂,很难在一个窗体中显示,则最好启用一个按钮,使用户能够在单独的窗体中查看结果。即使后台任务(如大型数据上载)没有可视结果,用户也应接收到后台任务已完成的确认信息。在这种情况下,Pocket PC 用户通知 API (SHNotificationAdd) 尤其有用,因为它可以提供便于查看又不会带来干扰的确认信息。

小结

.NET Framework 精简版丰富的异步 Web 服务和线程支持通过将长时间运行的进程移到后台,使开发日趋复杂的智能设备应用程序成为可能。决定在应用程序中引入后台处理时需要仔细规划,并理解可用的选项及其含义。如果使用得当,后台处理可以提高应用程序的响应性能,提供出色的用户体验,允许用户在不影响进程运行的情况下获得长时间运行的进程的结果。


版权所有:UML软件工程组织