Parallel Programming using TPL on WinForms

2019-03-21 16:33发布

问题:

I trying to use TPL on WinForms .NET 4.0, I followed this steps (go to the end of article) that are for WPF and made some small changes so it could work on WinForms but it still doesn't work.. It should display result on label and richTextBox but it not... I think the parallel process work cause mouse start moving slow for a while when I click the button..

public static double SumRootN(int root)
{   double result = 0;
    for (int i = 1; i < 10000000; i++)
    {   result += Math.Exp(Math.Log(i) / root);}
    return result;
}
private void button1_Click(object sender, EventArgs e)
{   richTextBox1.Text = "";
    label1.Text = "Milliseconds: ";
    var watch = Stopwatch.StartNew();
    List<Task> tasks = new List<Task>();
    for (int i = 2; i < 20; i++)
    {   int j = i;
        var t = Task.Factory.StartNew
          (   () =>
                {   var result = SumRootN(j);
                    Dispatcher.CurrentDispatcher.BeginInvoke
                        (new Action
                             (   () => richTextBox1.Text += "root " + j.ToString() 
                                   + " " + result.ToString() + Environment.NewLine
                             )
                         , null
                        );
                 }
            );
        tasks.Add(t);
    }
    Task.Factory.ContinueWhenAll
         (  tasks.ToArray()
            , result =>
                {   var time = watch.ElapsedMilliseconds;
                    Dispatcher.CurrentDispatcher.BeginInvoke
                          (   new Action
                                (    () =>
                                      label1.Text += time.ToString()
                                 )
                           );
                }
        );
}

回答1:

Your code will not work because the thread UI to display result totally different with WPF. With WPF the thread UI is Dispatcher but on Windows Form is another one.

I have modified your code to help it work.

    private void button1_Click(object sender, EventArgs e)
    {
        richTextBox1.Text = "";
        label1.Text = "Milliseconds: ";

        var watch = Stopwatch.StartNew();
        List<Task> tasks = new List<Task>();
        for (int i = 2; i < 20; i++)
        {
            int j = i;
            var t = Task.Factory.StartNew(() =>
            {
                var result = SumRootN(j);
                richTextBox1.Invoke(new Action(
                        () =>
                        richTextBox1.Text += "root " + j.ToString() + " " 
                              + result.ToString() + Environment.NewLine));
            });
            tasks.Add(t);
        }

        Task.Factory.ContinueWhenAll(tasks.ToArray(),
              result =>
              {
                  var time = watch.ElapsedMilliseconds;
                  label1.Invoke(new Action(() => label1.Text += time.ToString()));
              });
    }


回答2:

Leaving aside the whether it was good to do this way, from the learning angle and mentioned in comments topic "System.Windows.Threading.Dispatcher and WinForms?" with somewhat confusing answer:

"If you are sure to be in UI thread (eg. in an button.Click handler), Dispatcher.CurrentDispatcher gives you the UI thread dispatcher that you can use to dispatch from background thread to UI thread as usual"

it is pertinent to mention that (also provided my answer to mentioned above question):

  • Task.Factory.StartNew() spawns executions on multiple threads different from the main UI or its child threads
  • it is possible to use Dispatcher on any thread
  • WPF application OOTB (Out-of-thebox) provides System.Windows.Threading.Dispatcher DispatcherObject.Dispatcher of UI thread absent in Winfows forms
  • Used in question Dispatcher.CurrentDispatcher is getting the dispatchers on spawned by tasks non-ui threads

Anyway, from the didactic point of view of making minimum changes to the original WPF code, you should have caught and used UI dispatcher:

private void button1_Click(object sender, EventArgs e)
{   Dispatcher dispatcherUI = Dispatcher.CurrentDispatcher;//added **********
    richTextBox1.Text = "";
    label1.Text = "Milliseconds: ";
    var watch = Stopwatch.StartNew();
    List<Task> tasks = new List<Task>();
    for (int i = 2; i < 20; i++)
    {   int j = i;
        var t = Task.Factory.StartNew
          (   () =>
                {   var result = SumRootN(j);
      //Dispatcher.CurrentDispatcher.BeginInvoke//***changed to
                    dispatcherUI.BeginInvoke
                        (new Action
                             (   () => richTextBox1.Text += "root " + j.ToString() 
                                   + " " + result.ToString() + Environment.NewLine
                             )
                         , null
                        );
                 }
            );
        tasks.Add(t);
    }
    Task.Factory.ContinueWhenAll
         (  tasks.ToArray()
            , result =>
                {   var time = watch.ElapsedMilliseconds;
     //Dispatcher.CurrentDispatcher.BeginInvoke//**************changed to
                    dispatcherUI.BeginInvoke//added
                          (   new Action
                                (    () =>
                                      label1.Text += time.ToString()
                                 )
                           );
                }
        );
} 


回答3:

As described in the link below, the correct way would be to eliminate the use of Dispatcher class altogether.Instead, you should create a relevant instance of TaskScheduler and pass it to the Task methods. http://blogs.msdn.com/b/csharpfaq/archive/2010/06/18/parallel-programming-task-schedulers-and-synchronization-context.aspx

That is

Task.Factory.ContinueWhenAll(tasks.ToArray(),
      result =>
      {
          var time = watch.ElapsedMilliseconds;
          this.Dispatcher.BeginInvoke(new Action(() =>
              label1.Content += time.ToString()));
      });

would become

var ui = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.ContinueWhenAll(tasks.ToArray(),
    result =>
    {
        var time = watch.ElapsedMilliseconds;
        label1.Content += time.ToString();
    }, CancellationToken.None, TaskContinuationOptions.None, ui);