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 ProgressWorkerReportProgress = True, and wire up the ProgressChangedEvent
Support CancellingWorkerSupportsCancellation = True
Running without ParamNone
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版。(略)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章