きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

基礎学習 × .NET Framework の非同期処理を見直してみた × その4

この記事の目的

この記事では、

Taskクラスを使って画面の情報を更新する方法をまとめること

を目的としています。

ついでに、Windows Forms,WPF,UWPで非同期処理を実装する方法についても調べてみました。

本題

★ことの発端

GUIアプリケーション(特にUWP)にて、以下の処理を実装するために調べ始めました。

キャンセル可能なタスクを実行。
タスクを実行中は途中経過をUIに随時反映。
・タスクの途中でキャンセルされたとき、ContinueWithで指定したタスク(TaskContinuationOptions.OnlyOnCanceled)を実行。
ContinueWithで指定したタスクにより、画面に「キャンセルされました」メッセージを表示。

・タスクがキャンセルされずに完了した場合、ContinueWithで指定したタスク(TaskContinuationOptions.OnlyOnRanToCompletion)を実行。
ContinueWithで指定したタスクにより、画面に処理結果を表示。


★Taskクラスを使った場合

TaskのContinueWithメソッドに、TaskScheduler.FromCurrentSynchronizationContext()を渡すことで、UIスレッドで処理を実行することができます。
このTaskSchedulerは、規定では、スレッドプールタスクスケジューラーを利用します。
スレッドプールのタスクスケジューラーは、スレッドプールに対して、タスクをスケジュールするため、UIコントロールにアクセスしようとすると、例外が発生します。
実装イメージは以下の通りです。

 //TaskScheduler.Defaultを指定すると、ThradPoolのスレッドでタスクが実行されるため、以下の例外がスローされる。
 //System.Exception: 'アプリケーションは、別のスレッドにマーシャリングされたインターフェイスを呼び出しました。
 this.SampleTask.ContinueWith((task, status) =>
 {
     this.TaskJobStatus_TextBlock.Text = "canceled";//コントロールにアクセス
  }, new Object(),
  CancellationToken.None,
  TaskContinuationOptions.OnlyOnRanToCompletion,
  TaskScheduler.Default); //←ここ

何もしないままでは、Task実行中にUIコントロールを更新することはできませんが、Taskクラスには、UIコントロールにアクセスできるタスクをスケジュールするためのスケジューラーが用意されています。
それがTaskScheduler.FromCurrentSynchronizationContext()です。
このスケジューラーを利用することで、GUIスレッドにてタスクを実行することが可能となります。
先ほどのコードを書き換えると、以下のようになります。
この実装だと、UIコントロールにアクセスしても例外を発生させることなく実行することができます。

this.SampleTask.ContinueWith((task, status) =>
{
    this.TaskJobStatus_TextBlock.Text = MessageManager.WriteJobStatus("OnlyOnRanToCompletion");
    this.TaskJobStatus_TextBlock.Text += $"\r\nResult : {task.Result.ToString()}";
}, new Object(),
   CancellationToken.None,
   TaskContinuationOptions.OnlyOnRanToCompletion,
   TaskScheduler.FromCurrentSynchronizationContext()); //ここ

ただ、GUIスレッドを使うことになるため、重たい処理に対してこのオプションを指定すると、
画面が固まってしまいます。
画面を固めないようにするための非同期処理でもあるので、それでは本末転倒です(-_-メ)
あくまで、【処理結果】を画面に表示するときだけ利用するべきだと思います。

また、Taskのインスタンスを生成する時にはタスクスケジューラを指定できるオーバーロードがないため、TaskFactoryのStartNewを使う必要があります。
上記のコードでは、Taskのインスタンス生成時ではなくContinueWithしている時にタスクスケジューラのオプションを指定しています。
そのため、TaskFactoryは利用していません。



では、画面を固めないように、重たい処理中の途中経過を随時画面に反映するためには、どうしたらよいのか・・・と、いうことを次から見ていきます。
アプリケーションごとに違うので、Windows Forms、WPF、UWPと順番に見ていきます。

Windows Formsの場合

全てのコントロールに含まれるBeginInvokeメソッドを使うことで、
UIスレッドに対しコントロールの更新処理を実行することができます。
以下のブログを参考にしました。
Part 1. Windows フォームのマルチスレッド処理の基礎 – とあるコンサルタントのつぶやき

実装すると、以下のようになります。

private void LongTask()
 {
     for (int i = 0; i <= 100; i++)
     {
         // 何らかのタスクを実施...
         System.Threading.Thread.Sleep(100);
  
         // 進捗状態として画面を更新
         this.BeginInvoke(
             new UpdateProgressBarDelegate(UpdateProgressBar),
             new object[] { i });
     }
}
WPFの場合

System.Windows.Threading.Dispatcherを利用することで、
UIスレッドに対しコントロールの更新処理を実行することができます。
以下のブログを参考にしました。
非同期処理とディスパッチャーufcpp.wordpress.com


実装イメージは以下の通りです。

this.Dispatcher.BeginInvoke(new
      Action(() =>
      {
             //UIスレッドで実行すべき処理
       }));
});
★UWPの場合

最後に、UWPについてみていきます。
UWPも、WPFと同じようにDispatcherを使うことで、
UIスレッドに対しコントロールの更新処理を実行することができます。
UWPのDispacherは、Windows.UI.Core名前空間にあり、WPFのDispacherとは別のものです。
(プロパティ名がDispacherなだけで、クラス名はCoreDispatcherです。)

以下のように実装することができます。

 private int SampleMethod()
 {
     int sum = 0;
     int length = 10;
     for (int i = 0; i < length; i++)
      {
          Task.Run(async () =>
          {
              await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
              {
                  //ユーザーインターフェースを操作する
                  this.TaskStatus_TextBlock.Text = i.ToString();
              });
          });
          sum += i;
      }
      return sum;
}

まとめ

TaskからUIを更新するときは、「タスクスケジューラーのオプションを指定」。
Windows Formsのときは、「System.Windows.Forms.ControlクラスのBeginInvokeメソッド」。
WPFのときは、「System.Windows.Threading.DispatcherクラスのBeginInvokeメソッド」。
UWPのときは、「Windows.UI.Core.CoreDispatcherのRunAsyncメソッド」。



これを意識して、冒頭に書いた「やりたいこと」ができるアプリを作ってみようと思います( ゚Д゚)