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

This article is translated from Sacha Barber's article  Beginners Guide to Threading in .NET: Part 5 of n .

This series consists of 5 articles and this is the last one. The article goes from shallow to deep and introduces all aspects of multithreading, which is very good.

1)Why Thread UIs

If the UI of the application is unresponsive, the relationship between the thread and the UI may be considered at this time. How to avoid this, is to let background tasks run on a background thread, leaving only the UI to deal with user actions.

When the background thread's work is done, we allow it to update the UI at the right time.

This document mainly introduces the creation of UI to cooperate with single or multiple threads to ensure the sensitivity of UI, that is, UI will not get stuck. The article mainly involves WinForm, WPF and the key points of using Silverlight.

2)Threading in WinForms

This part will introduce how to use threads in the WinForms environment, involving the BackgroundWorker that appeared after .Net 2.0. This is by far the easiest way to create and control background threads and work with the UI. Of course, we won't go into a lot of convenience related to creating and controlling threads, just focusing on coordination with the UI.

BackgroundThread usage is not as convenient as you create your own thread, it is generally used in the following special cases:

a) Background work

b) Background app with parameters

c) show progress ·

d) End of report

e) Cancellable

If these meet your requirements, then choose BackgroundThread. Of course, it is up to us to control its subtlety. This article will use BackgroundWorker to complete background tasks, for more information on threads, you can find other articles in this series.

A) A bad instance

Add a textBox to a button on the interface, run the code, you will see


Of course, we can see the prompt when the thread ends: Completed background task.

The error message is obviously that in the background thread we accessed the txtResults of the UI thread. Below is the code we use:

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);
    }
}

We focused on the backgroundWorker1_DoWork() method, where we got an InvalidOperationException because, in the .Net programming environment, it is a basic principle that all controls can only be touched by the user who created it. In this instance, we didn't assign an action to deal with this when the background thread finished, and using it directly would have a nasty exception.

We can fix this exception in several ways, but before that, let's talk about 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) some good way

Way 1: Use BeginInvoke (for all versions of .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);
}

This can be seen as the oldest way to update UI.

Method 2: Use Synchronization Context (SynchronizationContext; for .Net 2.0 and above)

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);
}
The SynchronizationContext object allows us to manipulate the UI thread through the Send() method. Inside it just wraps some anonymous delegates.

Way 3: Using Lambdas (for .Net3.0 and above)

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);
}

Use lambdas instead of anonymous delegates. lambdas are good for small tasks, but are occasionally used for more complex jobs.

Here is the interface during normal operation:


C) section about update progress

The example code interface includes button Go, button Cancel and text box txtResults, the code is as follows:

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();
}

In the code, the progress is reported in BackgroundWorker.DoWork, and the value of the progress bar is updated in the ProgressChanged event (why can the interface be updated at this time, I don't understand?).


3)Threading in WPF

WPF appeared after .Net 3.0. Although the interface programming method is different, the thread operation part is the same. 'Controls can only be touched by the thread that was created' turns out the same applies to WPF. The only difference is that we have to use a WPF object Dispatcher, which manages the work queue on the thread.

The following code is using BackgroundWorker in WPF, the XAML code part is not shown:

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);
    }
}

This is similar to the above WinForm instance, the difference is that Dispatcher, CheckAccess() is similar to InvokeRequired in WinForm; another thing to note is that a delegate is packaged as System.Action, which includes a method with no parameters and no return value. Here's a comparison of key parts:

//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());

The above describes the use of backgroundWorker in WPF, and then the use of thread pools is introduced.

Way 1: Using 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);
}

Since it involves System.Action, it needs to be implemented in .Net 3.5 and above. The most important thing in the code is to obtain the state of WaitCallback. The state parameter is usually an object. Using lambda can reduce the amount of code. The above waitCB is the actual state object (Object type), so it needs to be unpacked. The most common way of WaitCallback is the following way two.

Approach 2: More explicit syntax

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());
}

Personally, I prefer the second method, which is intuitive.

4)Threading in Silverlight

Requires Silverlight 2.0 beta to be installed. (slightly)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325644270&siteId=291194637