[C#]System.Threading.Tasks.Taskを使用したプログレスバーの作成時のメモ。

このエントリーで作成したプログレスバーFormの仕組みを使って、ちょっとソースをいじっていたところ、System.Threading.Tasks.Taskクラスでハマった時のメモ。

やりたいこと
ある処理を実行している際にプログレスバーを表示させたい。処理はTaskクラスを使用して、結果はTask内で作成し、FormはそのTaskクラスの実行結果をWindowのタイトルバーに表示したい。なおFormはTaskを実行している時は固まったりしないこと。

だめなパターン
Taskクラスを使用してプログレスバーの進捗を行う仕組みを考えた時に以下のソースではうまくいきません。

ソース

        private void Form2_Load(object sender, EventArgs e)
        {
            Action<string> DispLog = (arg) => Trace.WriteLine(string.Format("ThreadID:{0}> {1}", Thread.CurrentThread.ManagedThreadId.ToString(), arg));

            DispLog("Form Start");

            var task = Task.Factory.StartNew(() =>
            {
                DispLog("Task Start");

                for (int i = 0; i < 100; i++)
                {
                    DispLog("loop:"+i.ToString());
                    System.Threading.Thread.Sleep(10);      // 重たい処理
                    this.Invoke(new Action<int>((p) =>
                    {
                        DispLog("ProgressChange:" + p.ToString());
                        progressBar1.Value = p + 1;
                    }), new object[] { i });
                }

                DispLog("Task End");
                return 0;
            });

            DispLog("Coffee break.");

            // taskの結果を待つ
            if (task.Result == 0)
            {
                DispLog("Task Result.");
                this.Text = "OK";
            }

            DispLog("Form End");

        }

実行結果

ThreadID:9> Form Start   // ID:9 Form
ThreadID:9> Coffee break.
ThreadID:6> Task Start     // ID:6 Task
ThreadID:6> loop:0

..デッドロック!!

デッドロックする理由
FormのスレッドIDは9,子Task(task)が6で、for文の1回目の”loop:0″を表示した後のthis.Invokeメソッド内はForm(ID:9)が呼ばれる状態ですが、Formは”Coffee break”を表示した後のtask.Resultでtask(ID:6)の終了待ち状態に入っているためデッドロックに陥ります。というか、親FormがTask待ちになってしまうこと自体が固まった状態だってことに気づかないなんて馬鹿ですねぇ~ → 私ですが何か。

this.BeginInvoke()に変えてみる。
this.Invoke()の部分を非同期呼び出しであるthis.BeginInvoke()に変えてみましょう。

ソース

                    this.BeginInvoke(new Action<int>((p) =>
                    {
                        DispLog("ProgressChange:" + p.ToString());
                        progressBar1.Value = p+1;
                    }),new object[]{i});

実行結果

ThreadID:9> Form Start      // ID:9 Form
ThreadID:9> Coffee break.
ThreadID:10> Task Start     // ID:10 Task
ThreadID:10> loop:0
ThreadID:10> loop:1
ThreadID:10> loop:2
...
ThreadID:10> loop:98
ThreadID:10> loop:99
ThreadID:10> Task End
ThreadID:9> Task Result.
ThreadID:9> Form End
ThreadID:9> ProgressChange:0
ThreadID:9> ProgressChange:1
...
ThreadID:9> ProgressChange:98
ThreadID:9> ProgressChange:99

今度はデッドロックには陥らないものthis.BeginInvoke()の実行タイミングがTask(ID:10)が全て終了した後に実行されています。実際の処理の結果は正確に取れるものの、メインの処理が実行中にプログレスバーが表示されることはありません。

改善策
プログレスバーFormをマルチスレッド化する理由は親のFormが固まらないようにするためです。なので、メインとなる処理を実行している別スレッド上のTaskの終了を待つにはTaskクラスで待たせればいいことになります。

Task待ちはTaskにまかせる

        private void Form2_Load(object sender, EventArgs e)
        {
            Action<string> DispLog = (arg) => Trace.WriteLine(string.Format("ThreadID:{0}> {1}", Thread.CurrentThread.ManagedThreadId.ToString(), arg));

            DispLog("Form Start");

            var task1 = Task.Factory.StartNew(() =>
            {
                DispLog("Task1 Start");

                var task2 = Task.Factory.StartNew(() =>
                    {
                        DispLog("Task2 Start");
                        for (int i = 0; i < 100; i++)
                        {
                            DispLog("loop:" + i.ToString());
                            System.Threading.Thread.Sleep(100);      // 重たい処理
                            this.Invoke(new Action<int>((p) =>
                            {
                                DispLog("ProgressChange:"+p.ToString());
                                progressBar1.Value = p+1;
                            }),new object[]{i});
                        }

                        DispLog("Task2 End");
                        return 0;
                    });

                DispLog("Coffee break.");

                // task2の結果を待つ
                if (task2.Result == 0)
                {
                    DispLog("Task2 Result.");
                    this.Invoke(new Action(() => this.Text = "OK"));
                }

                DispLog("Task1 End");

            });

            DispLog("Form End");

        }

実行結果

ThreadID:9> Form Start            // ID:9 Form
ThreadID:9> Form End
ThreadID:10> Task1 Start          // ID:10 Task1
ThreadID:10> Coffee break.
ThreadID:11> Task2 Start          // ID:11 Task2
ThreadID:11> loop:0
ThreadID:9> ProgressChange:0
ThreadID:11> loop:1
ThreadID:9> ProgressChange:1
...
ThreadID:11> loop:98
ThreadID:9> ProgressChange:98
ThreadID:11> loop:99
ThreadID:9> ProgressChange:99
ThreadID:11> Task2 End
ThreadID:10> Task2 Result.
ThreadID:10> Task1 End

Form(ID:9)はTask1(ID:10)を非同期で実行した後に”Form End”を表示して終了します。これでFormはどのTaskの終了待ち状態にならないため、固まったようになりません。Task1(ID:10)はTask2(ID:11)を非同期実行した後に、”Coffee break”を表示して、task2.Resultに値がセットされるまで終了待ちになります。Task2(ID:11)はfor文を実行させ、メインの処理( Sleep )を実行させながらthis.Invoke()でprogressBar1の進捗を進めていることがわかります。これで、ユーザにはスムーズに処理が進んでいるように表示させることができます。また、Task1(ID:10)はTask2(ID:11)が終了した後に、ちゃんと結果を受け取ることができています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA


This blog is kept spam free by WP-SpamFree.