C# Taskのキソ

C#には非同期処理を扱うThreadクラスがあります。Threadクラスは生身のThreadオブジェクトを表しますが、スレッドの生成・削除にはコストが掛かります。そのためThreadではなく、ThreadPoolにプールされているThreadを利用して処理を依頼するというのが定石でした。

C#4.0で追加されたTaskは内部的にはこのThreadPoolを使い、もう一段高い抽象層を作り出すのに一役買っています。ある処理が終わったら別のスレッドでこの処理を行い、、といったコードが書きやすくなっています。C#5.0で追加されたasync/awaitはこのTaskクラスをメソッド定義のように簡単に作れるようにしたものです。要はこのTaskクラス、使い方を十分理解しておく必要があるということです。

スレッドの処理には、スレッドの作成・開始,開始時への値の設定から始まり、進捗報告・キャンセル対応・完了時の値取得・待合わせ・例外処理といろんな要素があります。それらを盛り込んでみたソースはこちら。

 TaskBasicForm.cs

namespace Task1stTouch
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    public partial class TaskBasicForm : Form
    {
        private Task<SampleTaskResult> task;
        private CancellationTokenSource cancelSouce;
        private TaskScheduler uiTaskScheduler;
        private bool canceled;

        public TaskBasicForm(){
            InitializeComponent();
        }

        private void createAndStartbutton_Click(object sender, EventArgs e){
            // 初期化
            canceled = false;
            cancelSouce = new CancellationTokenSource();    // キャンセルサポート時に必要
            uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // UIコントロール操作に必要

            // 作成&開始
            task = Task<SampleTaskResult>.Factory.StartNew(
                TaskExecuteAsyncronously, 
                new SampleTaskArg() { Seed = 10 }, 
                cancelSouce.Token );

            // task.Waitで待てるが、UIスレッドで待つことになるためNG.
            // キャンセル時or完了時の処理はtask自体に登録しておく.
            // Task終了時に演算結果が出る場合、結果を使用する位置で待つ.
            cancelSouce.Token.Register(() => canceled = true);
            task.ContinueWith(_ => statusLabel.Text = canceled ? "キャンセルされました" : "完了しました", uiTaskScheduler);
        }

        // 非同期実行されるメソッド
        private SampleTaskResult TaskExecuteAsyncronously(object o)
        {
            // 引数取得
            SampleTaskArg arg = o as SampleTaskArg;
            for (int i = 0; i < 100; i ++)
            {
                // 進捗報告
                var progressbarUpdateTask = new Task( () => progressBar.Value = i+1);
                progressbarUpdateTask.Start(uiTaskScheduler);
                // キャンセル要求チェック
                if (cancelSouce.IsCancellationRequested){
                    return null;
                }
                // 本来したい重たい処理
                Thread.Sleep(20);
            }
            // 処理完了時の値
            return new SampleTaskResult() { Value = arg.Seed * 10 };
        }

        private void cancelButton_Click(object sender, EventArgs e)
        {
            if (cancelSouce == null){
                return;
            }
            cancelSouce.Cancel();
        }

        private void resultButton_Click(object sender, EventArgs e)
        {
            if (task == null || canceled){
                return;
            }
            // task.Resultにアクセスすると結果がまだの場合待ちます.
            statusLabel.Text = "結果は" + task.Result.Value + "です.";
        }
    }
}

dialog

まずTaskの作成と開始はそれぞれコンストラクタとStartメソッドで独立することもできますが上のように同時にもできます。非同期処理が値を返す場合はTask<T>、値を返さない場合はTaskとします。非同期処理開始後、完了をWaitで待つ例がありますが、WindowsFormsやWPF等のUIを持つアプリはメッセージポンプを回さないといけないのでWaitは禁物です。

キャンセルや進捗表示は準備が必要です。キャンセルはCancellationTokenSourceオブジェクトが必要です。非同期実行されるメソッド内部でキャンセル状態を繰り返しチェックする処理は必要です。処理が複雑になるならcancelTokenのThrowIfcancellationRequestedを使うとキャンセル要求チェック&キャンセル要求時は例外スローができ、すっきりします。

進捗表示はあまり誉められた方法ではありません。プログレスバーはUIスレッドのものなのでInvoke的なことが必要です。.NET4.5以降(?)にはEventProgress<T>というものもありますが.NET4.0時点ではこれを自作するか、上のようにUI更新タスクを投げまくるかしかありません。多分。

結果取得はtaksk.Resultだけ。非同期処理が完了していない場合、ここで待ちに入ります。そのため上で記載したとおり完了時に算出される計算結果は必要なタイミングで要求するのが良いでしょう。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中