C Sharpの基礎 - マルチスレッドとGUI

提供:MochiuWiki : SUSE, EC, PCB
2021年11月24日 (水) 18:07時点におけるWiki (トーク | 投稿記録)による版 (文字列「</source>」を「</syntaxhighlight>」に置換)
ナビゲーションに移動 検索に移動

概要

マルチスレッドにおいて、GUIコントロールを操作することは重要なテーマの1つであり、操作しなければならないケースは次の2つだと考えられる。

  • 処理結果をGUIコントロールに反映する
  • 処理を中止する


処理結果をGUIコントロールに反映するとは、結果をテキストボックスに表示する、または、処理の進捗をプログレスバーに表示する等である。
処理を中止するとは、処理対象のデータに不都合があって処理が不要になる、または、予想より処理時間が長くなり途中で中止する等である。
この場合、キャンセルボタンを押下することで中止の意図をアプリケーションに通知することになる。

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

下記のサンプルコードは、ボタンを押下するとTask.Runで並列処理した結果をテキストボックスに表示するプログラムの例である。
ポイントは、TaskSchedulerクラスの静的なFromCurrentSynchronizationContextメソッドを呼び出して、
GUIスレッドに割り当てられたデフォルトのタスクスケジューラを取得する。
GUIコントロール(この場合はテキストボックス)にアクセスするところでGUIスレッド上で動作するように指定する。

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

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

   await Task.Run(() =>
   {
      string 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 スレッド上で動作することを指定する
}
</syntaxhighlight>



Invokeメソッドを使う

Windows フォームアプリケーションの場合、GUIコントロールのInvokeメソッドを呼び出すと、
呼び出し元のオブジェクトのスレッドにおいてデリゲートを実行することができる。
実行結果は、前項とほぼ同じなので省略する。

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

   await Task.Run(() =>
   {
      string s = String.Format("index= {0}, ThreadId= {1}\r\n", i, Thread.CurrentThread.ManagedThreadId);

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



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

以下のサンプルコードは、ボタンを押下するとプログレスバーを進捗させる。
可読性を考慮して、実際に並列処理するコードなどは含めていない。
ポイントは、TaskSchedulerクラスのFromCurrentSynchronizationContextメソッドを呼び出して、
GUIスレッドに割り当てられたデフォルトのタスクスクジューラを取得して、タスクを作成するときのオプション設定に使用する。
また、進捗の対象とするループ内の子タスクとしてプログレスバーを進捗させるタスクを設定する。

<source lang="cpp">
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.Run(() =>
   {
      Parallel.For(0, 100, i =>
      {
         // プログレスバーを進捗させるためのタスクをForループ内におく
         Task.Run(() =>
         {
            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);
}
</syntaxhighlight>



ボタンを押下した時に処理を中止する

<source lang="cpp">
private void btnStart_Click(object sender, RoutedEventArgs e)
{
   source = new CancellationTokenSource();
   CancellationToken token = source.Token;

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

   // UIスレッドへのスケジュール用
   var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
   btnCancel.Enabled = true;
   // プログレスバー設定
   progressBar.Visibility = Visibility.Visible;
   progressBar.Minimum = 0;
   progressBar.Maximum = 100;
   progressBar.Value = 0;

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

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

         // プログレスバーを進捗させるためのタスクを For ループ内におく
         var progressTask = Task.Run(() =>
         {
            progressBar.Value += 1;
            System.Windows.Forms.Application.DoEvents(); // プログレスバーの進捗を表示するため
         }, token, TaskCreationOptions.None, taskScheduler);
         try
         {
            await progressTask;  // progressTaskの実行終了を待つ
         }
         catch (AggregateException ae)
         {
            ae.Handle((x) =>
            {
               return true;
            });
         }
      });
   });

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

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
   source.Cancel();
}
</syntaxhighlight>