並列処理における UI コントロールの操作

Last Updated 2011/09/21


並列処理のサンンプルコードはほとんどがコンソールアプリケーションです。しかし、私はコンソールアプリケーションを好みません。したがって、並列処理において UI コントロールにアクセスする手順は私にとってもっとも重要なテーマの一つです。

並列処理において、UI コントロールにアクセスしなければならないケースとは次の 2 つだと思います。

「処理結果を UI コントロールに反映する」とは、結果をテキストボックスに表示するとか、処理の進捗状況をプログレスバーに表示するなどが考えられます。「処理を中止する」は処理の対象のデータの入力に不都合があって処理自体が不要になる場合とか、予想より処理時間が長くなりそうなので、途中で中止する場合です。この場合はボタンコントロールをクリックすることで中止の意図をアプリケーションに通知することになるでしょう。



処理結果をテキストボックスに表示する

タスクの実行結果をテキストボックスに表示する機会は多いと思いますので、その基本的な手順を示します。以下のコード例は、WPF アプリケーションとして作りました。フォームに button1 と textBox を配置し、button1 をクリックすると Parallele.For メソッドで並列処理した結果を textBox に表示します。

WPF アプリケーションとことわりましたが、Windows フォームアプリケーションでも多分同じ手順でいいと思います。ポイントは TaskScheduler クラスの static な FromCurrentSynchronizationContext メソッドを呼び出して、UI スレッドに割り当てられたデフォルトのタスクスケジューラを取得します。UI コントロール(この場合は textBox)にアクセスするところで UI スレッド上で動作するように指定します。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  // UI スレッドのデフォルトのタスクスケジューラ
  var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

  textBox.Text = String.Format("UI ThreadId= {0}\r\n", Thread.CurrentThread.ManagedThreadId);

  Task.Factory.StartNew(() =>
  {
    string s = "";

    Parallel.For(0, 10, (i) =>
    {
      s += String.Format("index= {0}, ThreadId= {1}\r\n", i, Thread.CurrentThread.ManagedThreadId);
    });

    return s; // タスクの戻り値
  })
    .ContinueWith( t =>
    {
      textBox.Text += t.Result + "\r\n";
      textBox.Text += String.Format("UI ThreadId= {0}\r\n", Thread.CurrentThread.ManagedThreadId);
    }, taskScheduler); // UI スレッド上で動作することを指定する
}

実行結果:

UI ThreadId= 9
index= 0, ThreadId= 12
index= 1, ThreadId= 12
index= 2, ThreadId= 12
index= 3, ThreadId= 12
index= 4, ThreadId= 12
index= 5, ThreadId= 12
index= 6, ThreadId= 12
index= 7, ThreadId= 12
index= 9, ThreadId= 12
index= 8, ThreadId= 13

UI ThreadId= 9

継続タスクの UI スレッドの ID(= 9)が最初のものと同じになっていることを確認してください。


WPF アプリケーションにおいて Dispatcher.Invoke メソッドを使う

WPF アプリケーションの場合、DispatcherObject クラスを継承するクラス(Control クラスがそうなので、すべての UI コントロールと考えてよい)の Dispatcher プロパティから参照する Dispatcher オブジェクトの Invoke メソッドを呼び出すと、呼び出し元のオブジェクトのスレッドにおいてデリゲートを実行することができます。

以下のコードは、フォームに button1 と textBox を配置し、button1 をクリックすると、textBox に実行結果を表示します。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  textBox.Text = String.Format("UI ThreadId= {0}\r\n", Thread.CurrentThread.ManagedThreadId);

  Task.Factory.StartNew(() =>
  {
    string s = "";

    Parallel.For(0, 10, (i) =>
    {
      s += String.Format("index= {0}, ThreadId= {1}\r\n", i, Thread.CurrentThread.ManagedThreadId);
    });

    textBox.Dispatcher.Invoke(new Action(() =>
    {
      textBox.Text += s;
    })); 
  });
}

実行結果:

UI ThreadId= 10
index= 0, ThreadId= 6
index= 1, ThreadId= 6
index= 2, ThreadId= 6
index= 3, ThreadId= 6
index= 4, ThreadId= 6
index= 5, ThreadId= 6
index= 6, ThreadId= 6
index= 7, ThreadId= 6
index= 8, ThreadId= 6
index= 9, ThreadId= 6

Windows フォームアプリケーションにおいて Invoke メソッドを使う

Windows フォームアプリケーションの場合、Control クラス(つまり、すべての UI コントロール)の Invoke メソッドを呼び出すと、呼び出し元のオブジェクトのスレッドにおいてデリゲートを実行することができます。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  textBox.Text = String.Format("UI ThreadId= {0}\r\n", Thread.CurrentThread.ManagedThreadId);

  Task.Factory.StartNew(() =>
  {
    string s = "";

    Parallel.For(0, 10, (i) =>
    {
      s += String.Format("index= {0}, ThreadId= {1}\r\n", i, Thread.CurrentThread.ManagedThreadId);
    });

    textBox.Invoke(new Action(() => // ← この行に注目
    {
      textBox.Text += s;
    }));
  });
}

実行結果は、前項とほぼ同じなので省略します。


処理の進捗状況をプログレスバーに表示する

以下のコードは、フォームに button1 と progressBar を配置し、button1 をクリックするとプログレスバーを進捗させます。コードの見易さを考慮し、実際に並列処理するコードなどは含めませんでした。

コードのポイントは、TaskScheduler クラスの FromCurrentSynchronizationContext メソッドを呼び出して、UI スレッドに割り当てられたデフォルトのタスクスクジューラを取得し、タスクを作成するときのオプション設定に使用します。また、For ループなどの進捗の対象とするループ内の子タスクとしてプログレスバーを進捗させるタスクを設定します。

private void button1_Click(object sender, RoutedEventArgs e)
{
  // UI スレッドへのスケジュール用
  var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

  // プログレスバーの設定
  progressBar.Visibility = Visibility.Visible;
  progressBar.Minimum = 0;
  progressBar.Maximum = 100;
  progressBar.Value = 0;

  var task = Task.Factory.StartNew(() =>
  {
    Parallel.For(0, 100, (n) =>
      {
        Thread.Sleep(200); // n を使う時間のかかる処理のかわり

        // プログレスバーを進捗させるためのタスクを For ループ内におく
        Task.Factory.StartNew(() =>
        {
          progressBar.Value += 1;
          System.Windows.Forms.Application.DoEvents(); // プログレスバーの進捗を表示するため
        }, CancellationToken.None, TaskCreationOptions.None, taskScheduler);
      });
  });

  // 継続元のタスクが終了したあとの処理
  var continueTask = task.ContinueWith((t) =>
  {
    progressBar.Visibility = Visibility.Hidden;
    progressBar.Value = 0;
  }, taskScheduler);
}

ボタンコントロールをクリックしたときに処理を中止する

以下のコードは、フォームに button1 と progressBar を配置し、button1 をクリックするとプログレスバーを進捗させます。コードの見易さを考慮し、実際に並列処理するコードなどは含めませんでした。

namespace CancelTest
{
  public partial class CancelButton : Window
  {
    private CancellationTokenSource FTokenSource = null;

    public CancelButton()
    {
      InitializeComponent();
      btnCancel.IsEnabled = false;
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      FTokenSource = new CancellationTokenSource();
      CancellationToken token = FTokenSource.Token;

      // タスクをキャンセルしたときのアクションを登録する
      token.Register(() =>
      {
        progressBar.Visibility = Visibility.Hidden;
        progressBar.Value = 0;
        btnCancel.IsEnabled = false;
        textBox.Text = "キャンセルされました。";
      });

      // UI スレッドへのスケジュール用
      var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

      btnCancel.IsEnabled = true;

      // プログレスバー設定
      progressBar.Visibility = Visibility.Visible;
      progressBar.Minimum = 0;
      progressBar.Maximum = 100;
      progressBar.Value = 0;

      Debug.WriteLine(String.Format("UI ThreadId: {0}", Thread.CurrentThread.ManagedThreadId));

      var task = Task.Factory.StartNew(() =>
      {
        // キャンセルが要求されていたら、OperationCanceledException 例外を発生する
        token.ThrowIfCancellationRequested();

        Parallel.For(0, 100, (n) =>
        {
          Thread.Sleep(200); // n を使う時間のかかる処理のかわり

          // プログレスバーを進捗させるためのタスクを For ループ内におく
          var progressTask = Task.Factory.StartNew(() =>
          {
            progressBar.Value += 1;
            System.Windows.Forms.Application.DoEvents(); // プログレスバーの進捗を表示するため
          }, token, TaskCreationOptions.None, taskScheduler);

          try
          {
            progressTask.Wait();  // progressTask の実行終了を待つ
          }
          catch (AggregateException ae)
          {
            ae.Handle((x) =>
            {
              return true;
            });
          }
        });
      });

      // 継続元のタスクが正常に終了したあとの処理
      var continueTask = task.ContinueWith((t) =>
      {
        progressBar.Visibility = Visibility.Hidden;
        progressBar.Value = 0;
        btnCancel.IsEnabled = false;
        textBox.Text = "正常終了しました。";
      }, taskScheduler);
    }

    private void btnCancel_Click(object sender, RoutedEventArgs e)
    {
      FTokenSource.Cancel();
    }
  } // end of CancelButton class
} // end of namespace

Freezable クラス

唐突に Freezable クラスが出てきて奇妙に思われるでしょうが、グラフィックスに関係する処理を並列化しようとしていて気付いたことがあります。デバッグ中に、「親の Freezable とは異なるスレッドに属する DependencyObject を使用することはできません。」エラーになったからです。

そこで、あらためて Freezable クラスを調べてみたところ、Freeze メソッドを呼び出して Freezale オブジェクトをフリーズしておけば異なるスレッドからのアクセスが可能になることが分かりました。


注意事項

このページのテーマに関して何かいい情報はないかと WEB サイトをウロウロしていると、いくつかのコード例を手に入れることができました。コード自体はたしかに正常に動作するのですが、シーケンシャル処理との処理時間を比較してみると、シーケンシャル処理のほうが高速な場合があります。これでは並列処理のよいコード例とはいえません。

そこで、最初はシーケンシャル処理するコードを書いたあと、それを並列化し、実際に並列化の効果があるかどうかをチェックすることをおすすめします。.NET Framework 4.0 の並列化機能はブラックボックス部分が多いため、シーケンシャル処理のほうが安全性が高いと思います。処理時間の差が少ない場合は、あえて並列化する必要はないと考えます。

−以上−