BackgroundWorker组件进行异步操作UI

BackgroundWorker组件进行异步操作UI
一:概述
在应用程序中,可能会遇到一些执行耗时的功 能操作,比如数据下载、复杂计算及数据库事务等,一般这样的功能会在单独的线程上实现,执行结束后结果显示到用户界面上,这样可避免造成用户界面长时间无 响应情况。在.NET 2.0及以后的版本中,FCL提供了BackgroundWorker组件来方便的实现这些功能要求。
 
二:组件介绍
BackgroundWorker类位于System.ComponentModel 命名空间中,通过该类在单独的线程上执行操作实现基于事件的异步模式。下面对BackgroundWorker类的主要成员进行介绍。

1:BackgroundWorker类的第1个主要方法是RunWorkerAsync,该方法提交一个以异步方式启动运行操作的请求,发出请求后,将引发 DoWork 事件,在事件处理程序中开始执行异步操作代码。RunWorkerAsync 方法签名如下,
publicvoidRunWorkerAsync();
publicvoidRunWorkerAsync(Object argument);
如果异步操作需要操作参数,可以将其作为argument参数提供,由于参数类型为Object,因此访问时可能需要进行类型转换。

2:CancelAsync 方 法提交终止异步操作的请求,并将 CancellationPending 属性设置为 true。需要注意的是,CancelAsync 方法是否调用成功,同WorkerSupportsCancellation 属性相关,如果允许取消执行的异步操作,需将WorkerSupportsCancellation 属性设置为true,否则调用该方法将抛出异常。CancelAsync方法不含参数,方法签名如下,
publicvoid CancelAsync();
调用 CancelAsync 方法时,BackgroundWorker的 CancellationPending 属性值将被设置为true,因此在编写单独线程中执行的辅助方法时,代码中应定期检查 CancellationPending 属性,查看是否已将该属性设置为 true,如果为true,应该结束辅助方法的执行。有一点需要注意的是,DoWork 事件处理程序中的代码有可能在发出取消请求时已经完成处理工作,因此,DoWork事件处理程序或辅助方法可能会错过设置 CancellationPending属性为true的时机。在这种情况下,即使调用 CancelAsync方法发出了取消异步操作请求,RunWorkerCompleted 事件处理程序中RunWorkerCompletedEventArgs 参数的 Cancelled 标志也不会被设置为 true,这是在多线程编程中经常会出现的竞争条件问题,因此编写代码的时候需要考虑。

3:在执行异步操作时,如果需要跟踪异步操作执行进度,BackgroundWorker类提供了 ReportProgress 方法,调用该方法将引发 ProgressChanged 事件,通过注册该事件在事件处理程序中获取异步执行进度信息。方法签名如下:
publicvoidReportProgress(int percentProgress);
publicvoidReportProgress(int percentProgress,Object userState);
该方法包含两个版本,percentProgress表示进度百分比,取值为0-100,userState为可选参数表示自定义用户状态。
同CancelAsync 方法一样,BackgroundWorker的WorkerReportsProgress 属性设置为 true时,ReportProgress 方法才会调用成功,否则将引发InvalidOperationException异常。

4:上面已经提到了 BackgroundWorker的3个属性,CancellationPending用来提示操作是否已经取 消,WorkerReportsProgress和WorkerSupportsCancellation分别用来设置是否允许进度汇报和进行取消操作。
publicboolCancellationPending { get; }
publicboolWorkerReportsProgress { get; set; }
publicboolWorkerSupportsCancellation { get; set; }
另外一个会用到的属性是IsBusy,
publicbool IsBusy { get; }
通过该属性查询BackgroundWorker实例是否正在运行异步操作,如果 BackgroundWorker 正在运行异步操作,则为true,否则为false。

5:BackgroundWorker类包含3个事件,在事件处理程序中可进行异步操作辅助代码编写和同用户界面信息交互。
publiceventDoWorkEventHandler DoWork;
publiceventProgressChangedEventHandler ProgressChanged;
publiceventRunWorkerCompletedEventHandler RunWorkerCompleted;
DoWork事 件处理程序用来调用辅助方法进行实际处理操作,由于该事件处理程序在不同于UI的线程上执行,因此需要确保在 DoWork 事件处理程序中不操作任何用户界面对象。如果辅助方法需要参数支持,可以通过RunWorkerAsync方法传入,在 DoWork 事件处理程序中,通过 DoWorkEventArgs.Argument 属性提取该参数。在异步操作期间,可以通过 ProgressChanged事件处理程序获取异步操作进度信息,通过RunWorkerCompleted 事件处理程序获取异步操作结果信息,在ProgressChanged和RunWorkerCompleted的事件处理程序中可以安全的同用户界面进行 通信。

三:应用示例
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace BackgroundWorkerExample
{
    public class FibonacciForm : System.Windows.Forms.Form
    {
        private int numberToCompute = 0;
        private int highestPercentageReached = 0;

        private System.Windows.Forms.NumericUpDown numericUpDown1;
        private System.Windows.Forms.Button startAsyncButton;
        private System.Windows.Forms.Button cancelAsyncButton;
        private System.Windows.Forms.ProgressBar progressBar1;
        private System.Windows.Forms.Label resultLabel;
        private System.ComponentModel.BackgroundWorker backgroundWorker1;

        public FibonacciForm()
        {
            InitializeComponent();
            InitializeBackgoundWorker();
        }

        private void InitializeBackgoundWorker()
        {
            backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
            backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
            backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
        }

        private void startAsyncButton_Click(System.Object sender, System.EventArgs e)
        {
            resultLabel.Text = String.Empty;
            numberToCompute = (int)numericUpDown1.Value;
            highestPercentageReached = 0;
            backgroundWorker1.RunWorkerAsync(numberToCompute);
        }

        private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e)
        {
            this.backgroundWorker1.CancelAsync();
        }

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            e.Result = ComputeFibonacci((int)e.Argument, worker, e);
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message);
            }
            else if (e.Cancelled)
            {
                resultLabel.Text = "Canceled";
            }
            else
            {
                resultLabel.Text = e.Result.ToString();
            }
        }

        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            this.progressBar1.Value = e.ProgressPercentage;
        }

        long ComputeFibonacci(int n, BackgroundWorker worker, DoWorkEventArgs e)
        {
            if ((n < 0) || (n > 91))
            {
                throw new ArgumentException(
                    "value must be >= 0 and <= 91", "n");
            }

            long result = 0;

            if (worker.CancellationPending)
            {
                e.Cancel = true;
            }
            else
            {
                if (n < 2)
                {
                    result = 1;
                }
                else
                {
                    result = ComputeFibonacci(n - 1, worker, e) +
                             ComputeFibonacci(n - 2, worker, e);
                }

                int percentComplete = (int)((float)n / (float)numberToCompute * 100);
                if (percentComplete > highestPercentageReached)
                {
                    highestPercentageReached = percentComplete;
                    worker.ReportProgress(percentComplete);
                }
            }

            return result;
        }

        #region Windows Form Designer generated code
        private void InitializeComponent()
        {
            this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
            this.startAsyncButton = new System.Windows.Forms.Button();
            this.cancelAsyncButton = new System.Windows.Forms.Button();
            this.resultLabel = new System.Windows.Forms.Label();
            this.progressBar1 = new System.Windows.Forms.ProgressBar();
            this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
            ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
            this.SuspendLayout();
            //
            // numericUpDown1
            //
            this.numericUpDown1.Location = new System.Drawing.Point(16, 16);
            this.numericUpDown1.Maximum = new System.Decimal(new int[] {
            91,
            0,
            0,
            0});
            this.numericUpDown1.Minimum = new System.Decimal(new int[] {
            1,
            0,
            0,
            0});
            this.numericUpDown1.Name = "numericUpDown1";
            this.numericUpDown1.Size = new System.Drawing.Size(80, 20);
            this.numericUpDown1.TabIndex = 0;
            this.numericUpDown1.Value = new System.Decimal(new int[] {
            1,
            0,
            0,
            0});
            //
            // startAsyncButton
            //
            this.startAsyncButton.Location = new System.Drawing.Point(16, 72);
            this.startAsyncButton.Name = "startAsyncButton";
            this.startAsyncButton.Size = new System.Drawing.Size(120, 23);
            this.startAsyncButton.TabIndex = 1;
            this.startAsyncButton.Text = "Start Async";
            this.startAsyncButton.Click += new System.EventHandler(this.startAsyncButton_Click);
            //
            // cancelAsyncButton
            //
            this.cancelAsyncButton.Enabled = false;
            this.cancelAsyncButton.Location = new System.Drawing.Point(153, 72);
            this.cancelAsyncButton.Name = "cancelAsyncButton";
            this.cancelAsyncButton.Size = new System.Drawing.Size(119, 23);
            this.cancelAsyncButton.TabIndex = 2;
            this.cancelAsyncButton.Text = "Cancel Async";
            this.cancelAsyncButton.Click += new System.EventHandler(this.cancelAsyncButton_Click);
            //
            // resultLabel
            //
            this.resultLabel.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
            this.resultLabel.Location = new System.Drawing.Point(112, 16);
            this.resultLabel.Name = "resultLabel";
            this.resultLabel.Size = new System.Drawing.Size(160, 23);
            this.resultLabel.TabIndex = 3;
            this.resultLabel.Text = "(no result)";
            this.resultLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            //
            // progressBar1
            //
            this.progressBar1.Location = new System.Drawing.Point(18, 48);
            this.progressBar1.Name = "progressBar1";
            this.progressBar1.Size = new System.Drawing.Size(256, 8);
            this.progressBar1.Step = 2;
            this.progressBar1.TabIndex = 4;
            //
            // backgroundWorker1
            //
            this.backgroundWorker1.WorkerReportsProgress = true;
            this.backgroundWorker1.WorkerSupportsCancellation = true;
            //
            // FibonacciForm
            //
            this.ClientSize = new System.Drawing.Size(292, 118);
            this.Controls.Add(this.progressBar1);
            this.Controls.Add(this.resultLabel);
            this.Controls.Add(this.cancelAsyncButton);
            this.Controls.Add(this.startAsyncButton);
            this.Controls.Add(this.numericUpDown1);
            this.Name = "FibonacciForm";
            this.Text = "Fibonacci Calculator";
            ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
            this.ResumeLayout(false);

        }
        #endregion

        [STAThread]
        static void Main()
        {
            Application.Run(new FibonacciForm());
        }
    }
}

四:实现原理

在分析BackgroundWorker实 现原理之前,需要了解一下在.NET Framework 2.0版本中新增加的两个类。AsyncOperationManager 类和AsyncOperation 类都位于System.ComponentModel 命名空间中,AsyncOperation类提供了对异步操作的生存期进行跟踪的功能,包括操作进度通知和操作完成通知,并确保在正确的线程或上下文中调 用客户端的事件处理程序。
publicvoidPost(SendOrPostCallback d,Object arg);
publicvoidPostOperationCompleted(SendOrPostCallback d,Object arg);
通过在异步辅助代码中调用Post方法把进 度和中间结果报告给用户,如果是取消异步任务或提示异步任务已完成,则通过调用PostOperationCompleted方法结束异步操作的跟踪生命 期。在PostOperationCompleted方法调用后,AsyncOperation对象变得不再可用,再次访问将引发异常。在两个方法中都包 含SendOrPostCallback委托参数,签名如下,
publicdelegatevoidSendOrPostCallback(Object state);
SendOrPostCallback 委托用来表示在消息即将被调度到同步上下文时要执行的回调方法。
 
AsyncOperation类看上去很强大,不过有开发人员反映该类的.NET 2.0版本存在Bug,在3.0及后面的版本微软是否进行过更新还需进一步考证。笔者在控制台应用程序中进行测试,asyncOperation的Post方法递交的SendOrPostCallback委托不一定是在控制台主线程执行,通过访问System.Threading.Thread.CurrentThread.ManagedThreadId可以确认这一点,奇怪的是控制台程序未发现运行异常,这个可能是控制台程序执行方式不同于窗体程序的原因。
 
AsyncOperationManager 类为AsyncOperation对象的创建提供了便捷方式,通过CreateOperation方法可以创建多个AsyncOperation实例,实现对多个异步操作进行跟踪。
 
BackgroundWorker组件通过DoWork事件实现了在单独的线程上执行操作,其内部通过异步委托来完成,在BackgroundWorker类内部声明了WorkerThreadStartDelegate委托,并定义了threadStart成员变量,同时在构造函数中初始化threadStart。
private delegate void WorkerThreadStartDelegate(object argument);
private readonly WorkerThreadStartDelegate threadStart;
public BackgroundWorker()
{
    this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart);
    //…
}
BackgroundWorker通过调用RunWorkerAsync方法开始执行异步操作请求,并在方法体中调用threadStart.BeginInvoke方法实现异步调用。
public void RunWorkerAsync(object argument)
{
    if (this.isRunning)
    {
        throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning"));
    }
    this.isRunning = true;
    this.cancellationPending = false;
    this.asyncOperation = AsyncOperationManager.CreateOperation(null);
    this.threadStart.BeginInvoke(argument, null, null);
}
在threadStart委托中指定的WorkerThreadStart方法将触发DoWork事件,使用者通过注册DoWork事件执行异步代码的操作,从下面的代码可以看出在DoWork事件处理程序中不能访问UI元素的原因。
private void WorkerThreadStart(object argument)
{
    object result = null;
    Exception error = null;
    bool cancelled = false;
    try
    {
        DoWorkEventArgs e = new DoWorkEventArgs(argument);
        this.OnDoWork(e);
        if (e.Cancel)
        {
            cancelled = true;
        }
        else
        {
            result = e.Result;
        }
    }
    catch (Exception exception2)
    {
        error = exception2;
    }
    RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(result, error, cancelled);
    this.asyncOperation.PostOperationCompleted(this.operationCompleted, arg);
}
在上述代码中,this.OnDoWork(e)方法产生DoWork事件,DoWork事件处理程序执行完成后会判断在事件处理程序中是否对DoWorkEventArgs.Cancel属 性进行了设置,如果使用者调用了CancelAsync 方法那么DoWorkEventArgs.Cancel会被设置为true,事件处理程序正常执行完成时可以从 DoWorkEventArgs.Result得到执行结果,如果出现处理异常将扑获异常,所有需要的信息将包含在 RunWorkerCompletedEventArgs实例中,最后执行asyncOperation.PostOperationCompleted 方法产生RunWorkerCompleted 事件,因此在RunWorkerCompleted事件处理程序中可以获得取消操作、处理异常或处理结果的信息。
 
类似于RunWorkerCompleted事件的发生机制,对于异步操作进度通知事件发生通过ReportProgress方法实现。
public void ReportProgress(int percentProgress, object userState)
{
    if (!this.WorkerReportsProgress)
    {
        throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntReportProgress"));
    }
    ProgressChangedEventArgs arg = new ProgressChangedEventArgs(percentProgress, userState);
    if (this.asyncOperation != null)
    {
        this.asyncOperation.Post(this.progressReporter, arg);
    }
    else
    {
        this.progressReporter(arg);
    }
}
调用者在DoWork事件处理程序中通过调用ReportProgress方 法进行进度汇报,其内部通过asyncOperation.Post方法产生ProgressChanged 事件,如果asyncOperation为null,那么就调用progressReporter方法产生事件,但是调用 progressReporter方法产生事件明显存在问题,因为这样产生的事件所在线程同DoWork事件为同一线程,ProgressChanged 事件处理程序也会执行在DoWork线程同一上下文中,因此在ProgressChanged事件处理程序中访问ProgressBar控件将出现“线程 间操作无效: 从不是创建控件“progressBar1”的线程访问它。”的异常。笔者认为这样的处理是组件的一个Bug,如果asyncOperation为 null,更好的处理方式是抛出异常或不做通知处理。值得一提的是,在控制台应用程序中测试调用progressReporter方法不会出现“线程间操 作无效”的异常。
 
结合构造函数,下面的代码有助于进一步理解ProgressChanged事件和RunWorkerCompleted事件产生机制。
public BackgroundWorker()
{
    this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart);
    this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted);
    this.progressReporter = new SendOrPostCallback(this.ProgressReporter);
}
private void ProgressReporter(object arg)
{
    this.OnProgressChanged((ProgressChangedEventArgs)arg);
}
private void AsyncOperationCompleted(object arg)
{
    this.isRunning = false;
    this.cancellationPending = false;
    this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs)arg);
}
 
最后,看一下RunWorkerAsync方法和CancelAsync方法的实现。
public void RunWorkerAsync(object argument)
{
    if (this.isRunning)
    {
        throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerAlreadyRunning"));
    }
    this.isRunning = true;
    this.cancellationPending = false;
    this.asyncOperation = AsyncOperationManager.CreateOperation(null);
    this.threadStart.BeginInvoke(argument, null, null);
}
 
public void CancelAsync()
{
    if (!this.WorkerSupportsCancellation)
    {
        throw new InvalidOperationException(SR.GetString("BackgroundWorker_WorkerDoesntSupportCancellation"));
    }
    this.cancellationPending = true;
}
 
五:结束语

BackgroundWorker组件简化了基于事件的异步操作编程,根据其实现原理可进一步编写支持多任务的异步操作组件来更好的满足异步操作密集的应用开发需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值