解析VS.Net线程异步调用

WebService是微软.NET技术的一个亮点,它使得跨平台、跨语言、基于Internet上的互操作成为可能。Visual Studio.NET的IDE使得WebService的开发变得非常容易。程序员不需要直接面对SOAP, UDDI以及WSDL等繁琐的细节。但是由于WebService基于Internet的本质,使得对调用它的客户端程序提出了一些新的挑战。举例来说,调用WebService往往会经历较长的时延,或者是得不到响应(原因可能是Internet的连接问题,带宽问题,对方服务器过于繁忙或者是宕机等)。这样,如果客户端程序使用桌面程序中广泛采用的同步函数调用就会使得程序在等待返回结果时被“挂起”,不能响应用户的键盘和鼠标事件,用户甚至无法“放弃(Abort)”操作。这种情况对于用户来说是不可以接收的。在这种情况下,你可能要考虑使用异步方式来调用服务器端的WebMethod了。这样的话,你可以在发出调用后,不被挂起,而继续做其它事情。

异步调用的本质

有不少读者可能对"同步"和"异步"的概念以及回调函数等等术语还有些疑惑。这里就举个简单的例子说明一下。

比方说,你早上到了办公室,去打开水,打回水后然后沏茶。你同步调用打开水和沏茶两个函数。打开水完成后才可能沏茶。沏茶完成后你才可以看文件或是做其它工作。如果茶炉房出了些问题或者是人很多,那么你就要等着,直到打到了开水,你才可以回来干其它事情。在茶炉房等待开水的时候你什么也不能干。这种情况叫做被阻拦(Block)。准确的说就是执行函数的线程被阻拦。

要避免这种情况,你可以到达办公室后,说“王秘书,给我打壶开水去”。说完后你就可以开始看文件,做其他事了。等一会儿后,王秘书回来了,你就可以沏茶了。叫王秘书打开水就意味着你异步调用打开水这个函数。你不再担心在茶炉房等开水,你吩咐完王秘书后,你没有被阻拦,你可以马上开始做其它事情。但是开水总是要有个人去打,在这里王秘书是真正完成打开水函数的人。用计算机术语来将,王秘书可以是一个进程(Process)或是一个线程(Thread)。从理论上将,进程和线程都可以完成打开水这件事,但是就这类问题而言,使用进程是非常"昂贵"的,并且新的进程和原进程的通讯要复杂和慢的多,所以通常情况下,王秘书将会是一个线程。

如果你对王秘书说"王秘书,给我打开水。打回来后给我沏杯茶"。这种情况下,沏茶就是一个回调函数。沏茶将由王秘书这个"线程"来执行,在完成了打开水之后。

看起来很COOL是吧。有个"王秘书"使唤方便多了。但是别忘了王秘书出现后会有些其它问题。比如王秘书打回开水后给你沏茶时,你正用着茶几。她和你没有协调好撞在了一起,并把开水倒到了你的裤子上。因为办公室有和两个人,就出现了相互协调的问题。用计算机术语讲就是"多线程同步Multi-Thread Synchronization"。在计算机世界里,多个线程如何协调,同步是一个并不简单的问题。搞不好回造成信息紊乱,程序死锁。

让我们把这个问题想的再深一层。王秘书如果打开水的时候出了事打不回开水怎么办?你可能在开始的时候回对她说,"要是半个小时打不到水就算了"。这样就要求王秘书有时间概念。从计算机上讲,就是Timeout的机制。如果王秘书半个小时后没有回来,你可能想派刘秘书再去打。但是王秘书还拿着水壶呢(占着资源hold resources),这可怎么办?如果一切顺利,王秘书打完开水,沏完茶后该如何处理呢?是让她"走人"呢还是留着呢?因为你可能一会儿还会有拿报纸,寄邮件等等杂事。留着她也许还有用。可是王秘书会办其它事吗?如果不行的话,你可能还要用刘秘书,赵秘书等等来干每一项具体工作。另外公司里可能不止你一个"头头",孙总,李CEO,马董事长也需要人"侍候"。这时候成立一个"秘书处"可能比每人配备一堆秘书更经济更有效。因为使用秘书是要有代价的(线程的生成,释放,同步是要占用计算机CPU时间和内存等资源的)。这个"秘书处"在计算机里的对应物就是线程池(Thread Pool)。这是当今开发服务器端程序普遍采用的一个技术,目的是最大可能的提高程序性能,优化资源配置。

使用用户自定义"线程类(Thread)"

异步调用不仅在调用WebService的WebMethod时有用,在调用任何一个很费时的函数时,异步都是一个很好的选择。在.NET出现以前,我们可以生成新的线程,让这个线程来完成费时函数的调用,而主线程可以继续其它工作。比如在Java里,我们可以创建一个Thread类的子类或是创建一个实现了Runnable的类来完成这项工作。在.NET里,我们仍然可以这样做。如下面的小例程所示。

服务器端是一个非常简单的WebMethod,仅为示意:

//服务器端的程序
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using System.Threading;
namespace StockService
{
   public class StockPrice : System.Web.Services.WebService
   {
      public StockPrice()
      {
         InitializeComponent();
      }

      #region Component Designer generated code
      //Required by the Web Services Designer 
      private IContainer components = null;
            
      /// <summary>
      /// Required method for Designer support - do not modify
      /// the contents of this method with the code editor.
      /// </summary>
      private void InitializeComponent()
      {
      }

      /// <summary>
      /// Clean up any resources being used.
      /// </summary>
      protected override void Dispose( bool disposing )
      {
         if(disposing && components != null)
         {
            components.Dispose();
         }
         base.Dispose(disposing);      
      }
      
      #endregion

      [WebMethod]
      public double getStockPrice(String stockSymbol)
      {
        //sleep 5 seconds and return a dummy number
         Thread.Sleep(5000);
         return new Random().NextDouble() + 15.0;
      }
   }
}

客户端程序是一个C#写的简单的Windows Form程序。其的核心语句为:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Threading;
using System.Web.Services.Protocols;

namespace AsyncClient_01
{
    public class Form1 : System.Windows.Forms.Form
    {
        private System.Windows.Forms.TextBox messageBox;
        private System.Windows.Forms.Button button1;
        //。。。
        //省略了其它界面部分的程序
        //。。。
        private void button1_Click(object sender, System.EventArgs e)
        {
            //生成Mythread对象,由它去进行函数调用。同时注册事件响应函数
            //用来显示异步调用结果
            MyThread aThread = new MyThread("IBM");
            aThread.gotPrice += new EventHandler(this.displayPrice);
        }

        private void displayPrice(object sender, System.EventArgs e)
        {
            GotPriceEvent ge = e as GotPriceEvent;
            //这段程序是由MyThread内产生的线程来执行的。为了避免和程序的主线程
            //发生冲突,使用了LOCK机制。另外,股票价格是放在事件的参数传过来的。
            lock (this)
            {
                if (ge != null)
                    messageBox.Text +=
                        "The Price is: " + ge.Price + System.Environment.NewLine;
                else
                    messageBox.Text += "The Price is unknown";
            }
            //下面屏蔽的程序段是把更新界面的动作转到界面专用线程上来执行。
            //这样来避免多个线程对同一个变量进行改动的同步问题
            /*
            String text;
            if(ge!=null)         
               text = "The Price is: " + ge.Price + System.Environment.NewLine;
            else
               text = "The Price is unknown";
            messageBox.Invoke(new MethodInvoker
   (new Updater(text,messageBox).update));
             */
        }

        class Updater
        {
            private String m_text;
            private Control m_control;

            public Updater(String text, Control control)
            {
                m_text = text;
                m_control = control;
            }

            public void update()
            {
                m_control.Text += m_text;
            }
        }

        //用户自定义的类。创建函数内将生成一个新的线程
        class MyThread
        {
            private String m_stock;
            public event EventHandler gotPrice;

            public MyThread(String stockSymbol)
            {
                m_stock = stockSymbol;
                //生成一个线程,这个线程将执行getPrice()函数
                new Thread(new ThreadStart(this.getPrice)).Start();
            }

            public void getPrice()
            {
                localhost.StockPrice stockService = new localhost.StockPrice();
                double price = stockService.getStockPrice(m_stock);
                //函数调用结束后,触发事件
                gotPrice(this, new GotPriceEvent(price));
            }
        }

        //用户自定义的事件参数
        class GotPriceEvent : System.EventArgs
        {
            private double m_price;
            public GotPriceEvent(double price)
            {
                m_price = price;
            }
            public double Price
            {
                get { return m_price; }
            }
        }
    }

上面的程序算是"手工"异步调用。程序员控制线程的生成和多线程的同步问题。熟悉Java多线程编程的朋友会感觉非常的熟悉。但事实上,Microsoft并不鼓励你这样来写程序。因为他们认为多线程编程比较复杂而且容易出错,并且你的线程使用方法往往不够标准和优化。微软认为线程的生成和管理对一个程序的性能和质量是非常重要的,越复杂的程序就越明显。因此Microsoft创建了一整套线程生成和管理的服务,并鼓励你在此基础之上开发你的应用程序。其技术核心就是我前面提到的“秘书处(Thread pool 线程池)”。.NET替你管理这个"秘书处",它根据程序运行时的软硬件资源情况来决定"雇佣"多少"秘书"为最优。当你的程序被放置在多CPU的高档服务器上运行的时候,.NET会自动调整线程数量以最大限度的提升程序性能。程序员在普通工作站开发编程时将不必考虑这些问题。

使用.NET提供的异步调用服务

当你在Visual Studio中加一个Web引用(Reference)的时候,.NET在后台给你生成了一个代理类。令人惊讶的是异步调用的函数也自动被生成了。就前面给的Web Service而言,这个自动生成的代理类为。

//-----------------------------------------------------------------------
// <autogenerated>
//    This code was generated by a tool.
//    Runtime Version: 1.0.3705.209
//
//    Changes to this file may cause incorrect behavior and will be lost if
//    the code is regenerated.
// </autogenerated>
//-----------------------------------------------------------------------
//
// This source code was auto-generated by Microsoft.VSDesigner, Version
// 1.0.3705.209.
//
namespace AsyncClient_01.localhost {
    using System.Diagnostics;
    using System.Xml.Serialization;
    using System;
    using System.Web.Services.Protocols;
    using System.ComponentModel;
    using System.Web.Services;

    /// <remarks/>
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Web.Services.WebServiceBindingAttribute(Name = "StockPriceSoap", Namespace = "http://tempuri.org/")]
    public class StockPrice : System.Web.Services.Protocols.SoapHttpClientProtocol
    {
        /// <remarks/>
        public StockPrice()
        {
            this.Url = "http://localhost/StockService/StockService.asmx";
        }

        /// <remarks/>
        [System.Web.Services.Protocols.SoapDocumentMethodAttribute
            ("http://tempuri.org/getStockPrice",
            RequestNamespace = "http://tempuri.org/",
            ResponseNamespace = "http://tempuri.org/",
            Use = System.Web.Services.Description.SoapBindingUse.Literal,
            ParameterStyle =
            System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
        public System.Double getStockPrice(string stockSymbol)
        {
            object[] results = this.Invoke("getStockPrice", new object[] {
                  stockSymbol});
            return ((System.Double)(results[0]));
        }

        /// <remarks/>
        public System.IAsyncResult BegingetStockPrice(string stockSymbol, System.AsyncCallback callback, object asyncState)
        {
            return this.BeginInvoke("getStockPrice", new object[] {
                  stockSymbol}, callback, asyncState);
        }

        /// <remarks/>
        public System.Double EndgetStockPrice(System.IAsyncResult asyncResult)
        {
            object[] results = this.EndInvoke(asyncResult);
            return ((System.Double)(results[0]));
        }
    }
}

程序中的黑体部分就是关于异步调用函数的程序段。你调用BegingetStockPrice()后将返回一个具有IAsyncResult界面的对象。通过它你就可以最终拿到函数的运行结果。Microsoft提供的异步调用比较灵活,下面就几种常见的用法做个介绍。

1.使用Pooling方法得到返回结果

你调用BegingetStockPrice()后得到一个具有IAsyncResult界面的对象。它提供了IsCompleted的属性。当这个值为"True"的时候,你就可以通过调用EndgetStockPrice()来拿到函数运行的结果。

更重要的是,你可以在异步调用的时候"放弃Abort"调用。下面的程序段是用3个"按钮(Button)"来示意如何使用异步调用,检查调用是否结束以及放弃调用。

//客户端的WebService引用
private localhost.StockPrice m_stockService;
private IAsyncResult m_handle;  
//异步调用WebMethod
private void button2_Click(object sender, System.EventArgs e)
{
    m_stockService = new localhost.StockPrice();
    m_handle = m_stockService.BegingetStockPrice("IBM",null,null);
    messageBox.Text += "Function is invoked." + System.Environment.NewLine;
 }
//检查异步调用是否完成。如果完成的话,就取出调用结果
private void button3_Click(object sender, System.EventArgs e)
{
    if(m_handle==null)
    {
       MessageBox.Show(this, "No function is called!");
       return;
    }
    if(m_handle.IsCompleted == false)
       messageBox.Text += "Price is not ready yet"  + System.Environment.NewLine;
    else
    {
       double price = m_stockService.EndgetStockPrice(m_handle);
       messageBox.Text += "The Price is: " + price + System.Environment.NewLine;
    }
}
//放弃异步调用
private void button4_Click(object sender, System.EventArgs e)
{
    if(m_handle!=null)
    {
       WebClientAsyncResult result = (WebClientAsyncResult)m_handle;
       result.Abort();
       m_handle = null;
    }
    messageBox.Text += "Function call is aborted!" + System.Environment.NewLine;
}

2.使用WaitHandle

你调用BegingetStockPrice()后得到一个具有IAsyncResult界面的对象。它提供了AsyncWaitHandle的属性。调用它的WaitOne()函数可以使程序被阻拦直到另外一个线程函数调用完成。之后程序将继续往下执行。

private void button8_Click(object sender, System.EventArgs e)
{
    if(this.m_stockService ==null)
        m_stockService = new localhost.StockPrice();

    m_handle = m_stockService.BegingetStockPrice("IBM",null,null);
    messageBox.Text = "The function is called" + System.Environment.NewLine;
    m_handle.AsyncWaitHandle.WaitOne();
    double price = m_stockService.EndgetStockPrice(m_handle);
    messageBox.Text += "The price is: " + price + system.Environment.NewLine;   
}

从现象上看,和同步调用相比你并没有得到好处。程序等待的时候仍然处于"挂起"状态。但是在有些情况下,"等待"还是有它的特色的。比如说你可以连续调用三个WebMethod,如果每个函数费时5秒,那么使用同步的话总共会使用15秒钟。但如果使用异步的话,你可能只要等待5秒钟。当然这要使用WaitHandle提供的WaitAll()函数。如下所示:

private void button7_Click(object sender, System.EventArgs e)
{
    if (this.m_stockService == null)
        m_stockService = new localhost.StockPrice();
    
    IAsyncResult[] handles = new IAsyncResult[3];
    for (int i = 0; i < 3; i++)
        handles[i] = m_stockService.BegingetStockPrice("IBM", null, null);
    
    messageBox.Text = "3 function is called" + System.Environment.NewLine;
    WaitHandle[] WaitHandles = {handles[0].AsyncWaitHandle, handles[1].AsyncWaitHandle, handles[2].AsyncWaitHandle};
    //函数被阻拦,直到3个函数都执行完毕。WaitAny()函数情况类似,但有一个函数完成后
    //程序就解阻,继续往下执行
    WaitHandle.WaitAll(WaitHandles);

    double[] prices = new double[3];
    for (int i = 0; i < 3; i++)
    {
        prices[i] = m_stockService.EndgetStockPrice(handles[i]);
        messageBox.Text += "The price is: " + prices[i] + System.Environment.NewLine;
    }
}

3.使用回调函数(CallBack)

看到现在,你可能还没有感到满意。因为你异步调用了函数后,还要手工检查函数是否执行完毕,或者要处于等待状态。能否让函数完成后,自动显示结果或是做其它操作呢?答案是"能"的。回调函数就是做这种事情的。

还记得我们前面说的例子吗。"王秘书,给我打壶开水去。打回来后给我沏杯茶()"。王秘书打水完成后,还会执行沏杯茶()这个回调函数。如果你原意,你可以让王秘书干其它任何事情。如下所示:

private void button5_Click(object sender, System.EventArgs e)
{
    if (this.m_stockService == null)
        m_stockService = new localhost.StockPrice();
    //生成回调函数
    AsyncCallback cb = new AsyncCallback(this.callback);
    m_stockService.BegingetStockPrice("IBM", cb, DateTime.Now);
    messageBox.Text = "The function is called" + System.Environment.NewLine;
}

private void callback(IAsyncResult handle)
{
    double price = m_stockService.EndgetStockPrice(handle);
    lock (this)
    {
        messageBox.Text += "The price is: " + price + ". Reques time: " +
            handle.AsyncState.ToString() + ", and returned at: " +
            DateTime.Now.ToString() +
            System.Environment.NewLine;
    }
}

如果你喝茶比较讲究,你可能会说,"王秘书,给我打壶开水去()。打回来后给我沏杯茶(),要龙井茶"。那么这个"龙井茶"是否可以传给王秘书呢?这样她在打回开水后沏茶的时候就知道用什么茶叶了。答案是可以的。如上面的例程所示,DateTime.Now在异步调用的时候被传递给了新的线程。在回调函数里面,这个参数可以拿出来使用。handle.AsyncState.就是这个被传进的参数。.NET规定这个参数可以是任意一个对象。所以如果有多个参数要传递的话,可以使用数组,HashTable等等。理论上讲,你可以传递任何东西。

线程间同步协调问题

从上面的例子你可以看出,异步调用的本质是由另外一个线程在真正执行函数的调用,不管是你自己直接生成的线程还是.NET提供的。当一旦进入多线程编程后,最重要的问题就是线程间的协调和同步。要高度注意的是线程间的问题往往很隐蔽,很难发现。在单处理器平台上也许不会出现问题,但移植到多处理器平台上后也许就会显现出来。.NET提供了许多方法来进行多线程的保护和协调。限于篇幅,这里不一一赘诉了。

异步的WebService

上文讨论的是如何在客户端异步的调用服务器端的WebService。但是客户端的努力还终究是有一些局限的。比如在客户端调用一个费时的WebService时,你很可能希望客户端有个"状态条"来不断的指示函数调用的进度,并且还可以随时"放弃"调用。这个任务单凭客户端程序就很难就决。这种情形就要求服务器端能提供异步服务了。任何开发异步的WebService是一个比较复杂的问题,笔者将在后文中再作介绍。谢谢!

Contributors: FHL