xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

TPL入門 (15) - UIコンポーネントの操作

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が実装されています。非常に難易度が高いですが、タスクスケジューラーを独自に実装する必要がある方は参考にすると良いかと思います。