C#:多线程(Beginners Guide to Threading in .NET: Part 5 of n)

本文翻译自Sacha Barber的文章 Beginners Guide to Threading in .NET: Part 5 of n

这个系列包括5篇文章,这是最后一篇。文章由浅入深,介绍了有关多线程的方方面面,很不错。

1)Why Thread UIs

应用程序如果出现UI反应迟钝,这个时候就可能考虑线程与UI的关系了。如何避免这种情况,那就是让后台任务在后台线程运行,只留下UI来应对用户的操作。

当后台线程工作完成时,我们允许它在合适的时间更新UI。

这篇文件主要介绍创建UI使之与单个或多个线程协作,来保证UI的灵敏度,也就是UI不会卡死。文章主要涉及WinForm,WPF及使用Silverlight的关键点。

2)Threading in WinForms

这一部分将会介绍在如何在WinForms环境下使用线程,涉及到.Net 2.0之后才出现的BackgroundWorker。这是目前为止使用最简单的方式创建和控制后台线程并与UI协同。当然,我们不会涉及很多创建和控制线程相关的便利,只关注与UI的协同。

BackgroundThread使用并不像你创建自己的线程那样便利,它一般使用与以下特殊场合:

a)后台工作

b)带参数的后台应用

c)显示进展·

d)报告结束

e)可以取消的

如果这些都满足你的要求,那选择BackgroundThread那就对了。当然,对于其精妙的控制,那就要靠我们自己了。这篇文章将会使用BackgroundWorker来完成后台任务,至于有关线程更多信息,你可以查找本系列的其它文章。

A)一个不好的实例

界面上就一个按钮加textBox,运行代码,将会看到


当然,线程结束时我们是能看到提示的:Completed background task。

错误提示很显然是在后台线程中我们访问了UI线程的txtResults。下面为我们使用的代码:

public partial class BackgroundWorkerBadExample : Form
{
    public BackgroundWorkerBadExample()
    {
        InitializeComponent();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            for (int i = 0; i < (int)e.Argument; i++)
            {
                txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
            }
        }
        catch (InvalidOperationException oex)
        {
            MessageBox.Show(oex.Message);
        }
    }

    private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        MessageBox.Show("Completed background task");
    }

    private void btnGo_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync(100);
    }
}

我们重点关注backgroundWorker1_DoWork()方法,在这里我们得到了一个异常InvalidOperationException,这是由于,在.Net编程环境,有一项基本原则就是所有的控件只能由创建它的用户接触。在这个实例中,在后台线程完成时我们没有指派相关操作来应对此事,直接使用的话就会出现令人讨厌的异常。

我们可以使用几种方式来修复这种异常,在这之前,我们先来讲讲BackgroundWorker:

Task

What needs Setting

Report Progress WorkerReportProgress = True, and wire up the ProgressChangedEvent
Support Cancelling WorkerSupportsCancellation = True
Running without Param None
Running with Param

None







B)一些好的方式

方式1:使用BeginInvoke(适用于所有版本的.Net)

try
{
     for (int i = 0; i < (int)e.Argument; i++)
     {
          if (this.InvokeRequired)
          {
              this.Invoke(new EventHandler(delegate
              {
                   txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
              }));
          }
          else
              txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
     }
}
catch (InvalidOperationException oex)
{
     MessageBox.Show(oex.Message);
}

这可以看做更新UI最古老的方式。

方式2:使用同步上下文(SynchronizationContext;适用于.Net 2.0及以上)

private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
    context = new SynchronizationContext();
}
.....
.....
try
{
    for (int i = 0; i < (int)e.Argument; i++)
    {
        context.Send(new SendOrPostCallback(delegate(object state)
        {
            txtResults.Text += string.Format(
                      "processing {0}\r\n", i.ToString());

        }), null);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}
SynchronizationContext对象允许我们通过Send()方法来操作UI线程。在其内部只是包装了一些匿名委托。

方式3:使用Lambdas(适用于.Net3.0及以上)

private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
    context = new SynchronizationContext();
}
.....
.....
try
{
    for (int i = 0; i < (int)e.Argument; i++)
    {
        context.Send(new SendOrPostCallback((s) =>
            txtResults.Text += string.Format(
                          "processing {0}\r\n", i.ToString())
        ), null);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

使用lambda来代替匿名委托。lambda适用于小任务,但偶有人用于较复杂的工作。

下面是正常运行时的界面:


C)有关更新进度部分

实例代码界面上包括按钮Go,按钮Cancel及文本框txtResults,代码如下:

private int factor = 0;
private SynchronizationContext context;

public BackgroundWorkerReportingProgress()
{
    InitializeComponent();

    //set up the SynchronizationContext
    context = SynchronizationContext.Current;
    if (context == null)
    {
          context = new SynchronizationContext();
    }
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    try
    {
         for (int i = 0; i < (int)e.Argument; i++)
         {
             if (worker.CancellationPending)
             {
                  e.Cancel = true;
                  return;
             }

             context.Send(new SendOrPostCallback( (s) =>
                        txtResults.Text += string.Format(
                        "processing {0}\r\n", i.ToString())
                    ), null);

             //report progress
             Thread.Sleep(1000);
             worker.ReportProgress((100 / factor) * i + 1);
         }
     }
     catch (InvalidOperationException oex)
     {
          MessageBox.Show(oex.Message);
      }
}
private void btnGo_Click(object sender, EventArgs e)
{
      factor = 100;
      backgroundWorker1.RunWorkerAsync(factor);
}

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

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
      MessageBox.Show("Completed background task");
}

private void btnCancel_Click(object sender, EventArgs e)
{
      backgroundWorker1.CancelAsync();
}

代码中在BackgroundWorker.DoWork中报告进展,在ProgressChanged事件中更新进度条的值(这个时候为啥能更新界面,不明白?)。


3)Threading in WPF

WPF出现于.Net 3.0之后,虽然界面编程方式不同,但线程操作部分是相同的。’控件只能由创建的线程来接触‘原来同样适用于WPF。唯一不同的是我们必须使用一个WPF对象Dispatcher,后者管理线程上的工作队列。

以下代码是在WPF中使用BackgroundWorker,XAML代码部分没有显示:

public partial class BackGroundWorkerWindow : Window
{
    private BackgroundWorker worker = new BackgroundWorker();

    public BackGroundWorkerWindow()
    {
        InitializeComponent();

        //Do some work with the Background Worker that 
        //needs to update the UI. 
        //In this example we are using the System.Action delegate.
        //Which encapsulates a a method that takes no params and 
        //returns no value.

        //Action is a new in .NET 3.5
        worker.DoWork += (s, e) =>
        {
            try
            {
                for (int i = 0; i < (int)e.Argument; i++)
                {
                    if (!txtResults.CheckAccess())
                    {
                        Dispatcher.Invoke(DispatcherPriority.Send,
                            (Action)delegate
                            {
                                txtResults.Text += string.Format(
                                    "processing {0}\r\n", i.ToString());
                            });
                    }
                    else
                        txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
                    }
                }
                catch (InvalidOperationException oex)
                {
                    MessageBox.Show(oex.Message);
                }
            };
    }

    private void btnGo_Click(object sender, RoutedEventArgs e)
    {
        worker.RunWorkerAsync(100);
    }
}

这与上面的WinForm实例相似,区别在于Dispatcher,CheckAccess()类似于WinForm中的InvokeRequired;另外需要注意的是把一个委托打包成System.Action,这个委托包括了一个无参无返回值的方法。下面是关键部分的对照:

//WPF
if (!txtResults.CheckAccess())
{
      Dispatcher.Invoke(DispatcherPriority.Send,
                          (Action)delegate
                          {
                                txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
                          });
}
else
      txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
//WinForm
if (this.InvokeRequired)
{
    this.Invoke(new EventHandler(delegate
    {
        txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
    }));
}
else
    txtResults.Text += string.Format("processing {0}\r\n", i.ToString());

以上介绍了WPF中使用backgroundWorker,接下来介绍使用使用线程池来实现.

方式1:使用Lambdas

try
{
    for (int i = 0; i < 10; i++)
    {
        //CheckAccess(), which is rather strangely marked [Browsable(false)]
        //checks to see if an invoke is required
        //and where i respresents the State passed to the 
        //WaitCallback        
        if (!txtResults.CheckAccess())
        {
            //use a lambda, which represents the WaitCallback
            //required by the ThreadPool.QueueUserWorkItem() method
            ThreadPool.QueueUserWorkItem(waitCB =>
            {
                int state = (int)waitCB;

                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    ((Action)delegate
                    {
                        txtResults.Text += string.Format(
                            "processing {0}\r\n", state.ToString());
                    }));
            }, i);
        }
        else
            txtResults.Text += string.Format(
                "processing {0}\r\n", i.ToString());
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}

由于涉及到System.Action,因此需在.Net 3.5及以上实现。代码中最为主的是取得WaitCallback的状态,状态参数通常是个object,使用lambda可以减小代码量。上面的waitCB就是实际的状态对象(Object类型),所以需要拆包操作。WaitCallback最常用的方式是以下方式二。

方式2:更加明了的语法

try
{
    for (int i = 0; i < 10; i++)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), i);
    }
}
catch (InvalidOperationException oex)
{
    MessageBox.Show(oex.Message);
}
....
....
....
// This is called by the ThreadPool when the queued QueueUserWorkItem
// is run. This is slightly longer syntax than dealing with the Lambda/
// System.Action combo. But it is perhaps more readable and easier to
// follow/debug
private void ThreadProc(Object stateInfo)
{
    //get the state object
    int state = (int)stateInfo;

    //CheckAccess(), which is rather strangely marked [Browsable(false)]
    //checks to see if an invoke is required
    if (!txtResults.CheckAccess())
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Normal,
            ((Action)delegate
            {
                txtResults.Text += string.Format(
                    "processing {0}\r\n", state.ToString());
            }));
    }
    else
        txtResults.Text += string.Format(
            "processing {0}\r\n", state.ToString());
}

个人比较喜欢方式二,直观。

4)Threading in Silverlight

需要安装Silverlight 2.0 beta版。(略)

猜你喜欢

转载自blog.csdn.net/huan_126/article/details/80189647