タスクを使ってUIをフリーズさせずに負荷の高い処理をする

なんの工夫もなく負荷の高い処理を記述すると、その間はユーザーインターフェースがフリーズします。

せっかくウインドウ形式のプログラムを書いているのに、ウインドウが動かせないとか最悪です。

こういった場合は、スレッドを使ってユーザーインターフェースから切り離して負荷の高い処理を実行します。とはいえ、開発チーム要員のスキルレベルが一定ではない場合、スレッドを使ったコードを書くとバグの温床になってしまいます。

ここでマイクロソフトさんが提案してくれたC#言語の機能が、タスク Task です。実際のところはスレッドなのですが、スレッドのコードを書いているという感覚が希薄なプログラミングが可能です。

とはいえ、やはりスレッドなので、ユーザーインターフェースのリフレッシュなどのコードを書くには、ひと手間が必要です。

下記のようなプログラムを作った場合のコードを示します。Form1 に button1, button2, button3 を配置して試してみてください。

button1 が、何も考えずに適当に書いたコードです。
button2 が、タスクを開始して、その終了待ちをしてからメッセージボックスを表示するコードです。
button3 が、タスクを開始した直後にメッセージボックスを表示して、タスクの終了待ちは気にしないコードです。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace my_app
{
	public partial class Form1 : Form
	{

		const int LOOP = 255;
		const int MSEC_WAIT = 10;

		int Counter = 0;

		public Form1()
		{
			InitializeComponent();

			button1.Text = "normal.";
			button2.Text = "await Task.Run()";
			button3.Text = "Task.Run()";

		}

		private void button1_Click( object sender, EventArgs e )
		{

			// UIがフリーズする.

			Counter = 0;

			for ( int n = 0; n < LOOP; n++ )
			{
				System.Threading.Thread.Sleep( MSEC_WAIT );
				Counter++;

				this.Invalidate( true );
			}

			// ループが終わってから表示される.
			MessageBox.Show( "After LOOP." );

		}

		// async が付加されていることに注目.
		private async void button2_Click( object sender, EventArgs e )
		{

			// UIがフリーズしない.

			// await で Task の終了待ちをするのでメッセージボックスはループ完了後に表示される.
			// button2_Click の前に async がついていることに注目.

			// await をつけるとタスクの終了を待つ.
			await Task.Run(() => {

				Counter = 0;

				for ( int n = 0; n < LOOP; n++ )
				{

					System.Threading.Thread.Sleep( MSEC_WAIT );
					Counter++;

					this.Invoke((Action)(() =>
					{
						this.Invalidate();
					}));

				}

			});

			// タスクが終わってから表示される.
			MessageBox.Show( "After Task.Run();" );

		}

		private void button3_Click( object sender, EventArgs e )
		{

			// UIがフリーズしない.

			// await がないのでタスクの終了待ちをしない.
			Task.Run(() => {

				Counter = 0;

				for ( int n = 0; n < LOOP; n++ )
				{

					System.Threading.Thread.Sleep( MSEC_WAIT );
					Counter++;

					this.Invoke((Action)(() =>
					{
						this.Invalidate();
					}));

				}

			});

			// タスクの開始直後に表示される.
			MessageBox.Show( "After Task.Run();" );

		}

		private void Form1_Paint( object sender, PaintEventArgs e )
		{

			// タイトルバーにカウンタ値を表示する.
			this.Text = Counter.ToString();

			// ウインドウ色を変更する.
			byte level = (byte)( Counter );
			this.BackColor = System.Drawing.Color.FromArgb( 0x00, level, level );

		}
	}
}