GUIアプリケーションを作成していると、非同期処理の前後や実行中にボタンなどのUIコンポーネントを制御したいと思うことは多々あると思います。しかし、Windows Forms、WPFなどでは、UIスレッド以外のスレッドから直接UIコンポーネントを操作することは認められていません (別スレッドからUIコンポーネントを操作しようとすると、InvalidOperationExceptionがスローされます)。そこで今回は、タスク上からUIコンポーネントを操作する方法について見ていきます。
タスクスケジューラー
これまでにも何度かスケジューラーという言葉を使ってきました。TPLにはタスクスケジューラーという概念があり、これがタスクの実行順序を決めたり、スレッドに振り分けたりします。その他にも、Visual Studioのデバッガーに管理しているタスクの情報を公開したりもます。タスクスケジューラーはTaskSchedulerクラスで表され、TPL標準ではスレッドプールタスクスケジューラーと同期コンテキストタスクスケジューラーが提供されています。
TPLの既定はスレッドプールタスクスケジューラーで、静的なTaskScheduler.Defaultプロパティで取得できます。スレッドプールタスクスケジューラーは、その名の通りタスクをスレッドプールのワーカースレッドに登録して処理を行います。
同期コンテキストタスクスケジューラーは、静的なTaskScheduler.FromCurrentSynchronizationContextメソッドで取得できます。このスケジューラーはWindows FormsやWPFなどのGUIアプリケーションで主に使用され、ボタンやメニューなどのUIコンポーネントをタスク上から更新できるように、タスクをアプリケーションのUIスレッドに登録して処理を行います。スレッドプールは一切使用しません。
同期コンテキストタスクスケジューラーの利用
上記で説明した通り、同期コンテキストタスクスケジューラーを使用することでUIコンポーネントを操作することができます。以下のサンプルを見てください。
using System; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Sample28_SynchronizationContextTaskScheduler { public partial class MainForm : Form { public MainForm() { this.InitializeComponent(); this.button.Click += this.Button_Click; } private void Button_Click(object sender, EventArgs e) { this.button.Enabled = false; Task.Factory.StartNew(() => { Thread.Sleep(3000); //---- Do Somothing }) .ContinueWith(parent => { this.button.Enabled = true; }, TaskScheduler.FromCurrentSynchronizationContext()); } } }
上記のサンプルでは、継続タスクを同期コンテキストタスクスケジューラーによる制御とすることで、継続タスク上でボタンのEnabledプロパティを操作しています。継続元のタスクはスレッドプールタスクスケジューラーによって制御されていることに注意してください。
「そんな面倒なことをするくらいなら、最初からすべて同期コンテキストタスクスケジューラーを利用すればいいじゃないか」と考えてしまうかもしれません。しかし、次のサンプルのようにしてしまうとGUIスレッドがブロックされ、タスク実行中にウィンドウを操作できなくなります。先程説明した通り、同期コンテキストタスクスケジューラーはGUIスレッド上でタスクを処理するように制御するためです。タスクを利用しTPLの恩恵を最大に受けたい場合は、可能な限り既定のスレッドプールタスクスケジューラーを利用するようにしてください。
using System; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication { public partial class MainForm : Form { public MainForm() { this.InitializeComponent(); this.button.Click += this.Button_Click; } private void Button_Click(object sender, EventArgs e) { this.button.Enabled = false; var task = new Task(() => { Thread.Sleep(3000); //---- Do Somothing this.button.Enabled = true; }); task.Start(TaskScheduler.FromCurrentSynchronizationContext()); } } }
Control.Invokeメソッドの利用 (Windows Forms)
TPL導入以前から存在する手法ですが、Windows Formsに限っての内容で別スレッド上からUIコンポーネントを操作する方法を紹介します。Control.Invokeメソッドを利用し、コントロールの基になるウィンドウハンドルを所有するスレッド上でデリゲートを実行する方法です。
using System; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Sample29_ControlInvoke { public partial class MainForm : Form { public MainForm() { this.InitializeComponent(); this.button.Click += this.Button_Click; } private void Button_Click(object sender, EventArgs e) { this.button.Enabled = false; Task.Factory.StartNew(() => { for (int i = 0; i <= 100; i++) { this.SyncInvoke(() => this.Text = string.Format("Progress : {0}%", i)); Thread.Sleep(50); } this.SyncInvoke(() => this.button.Enabled = true); }); } private void SyncInvoke(Action action) { if (this.InvokeRequired) this.Invoke(action); else action(); } } }
上のサンプルのように、タスク実行時に逐次進捗状態を通知するなどのコールバックが必要な場合に利用できます。この方法はWindows Formsでの非同期処理では比較的一般的な方法ですが、進捗状態をコールバックするようなシーンではBackgroundWorkerの方がお手軽です。
独自のタスクスケジューラー
タスクは非常に柔軟性が高く、TaskSchedulerクラスを継承した独自のタスクスケジューラーを作成することでより力を発揮できる場合があります。Microsoftが提供している並列プログラミングのサンプルであるSamples for Parallel Programming with the .NET FrameworkにParallelExtensionsExtrasがあり、その中に多くのTaskSchedulerが実装されています。非常に難易度が高いですが、タスクスケジューラーを独自に実装する必要がある方は参考にすると良いかと思います。