C# と VB.NET の質問掲示板

わんくま同盟

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

ツリー一括表示

async/awaitのWaitAllの挙動について /ぱっくん (20/02/09(Sun) 15:33) #93833
Re[1]: async/awaitのWaitAllの挙動について /Hongliang (20/02/09(Sun) 21:38) #93834
  └ Re[2]: async/awaitのWaitAllの挙動について /ぱっくん (20/02/10(Mon) 13:48) #93845 解決済み


親記事 / ▼[ 93834 ]
■93833 / 親階層)  async/awaitのWaitAllの挙動について
□投稿者/ ぱっくん (1回)-(2020/02/09(Sun) 15:33:32)

分類:[C#] 

Windows 10, 日本語 (64bit)の環境でVisual Studio 2015を使用しています。

async/awaitを用いた非同期処理を始めて実装しようとしているのですが、Windowsフォームアプリケーションで作成したコードでTask.WaitAllが実行されていない?実行完了しても抜けない?問題に悩まされています。
処理が5分ほどかかる複数の処理を同時並列し、すべて終わるまで待つというコードを作成しようとしており、処理をひとつづつtaskListに格納し、Task.WaitAll(taskList.ToArray());で一度に実行することを意図しています。

まずは、成功例から。下記のコンソールアプリケーションではタスクC,D,E,F,Gを非同期で実行し、すべての処理完了後、WaitAllを抜けます。

Program.cs
            var process2 = new TestRunner();
            process2.RunTest();

TestRunner.cs
        private DoLongTimeWorkers DoWork = new DoLongTimeWorkers();

        public void RunTest()
        {
            ExecuteLongWork("[Single Task] Process A:", 5, 590, 1000);
            ExecuteLongWork("[Single Task] Process B:", 6, 690, 1000);

            var taskList = new List<Task<string>>();
            
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process C:", 12, 990, 3000));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process D:", 26, 520, 8000));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process E:", 18, 800, 1000));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process F:", 21, 600, 6000));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process G:", 58, 200, 6000));

            Task.WaitAll(taskList.ToArray());
        }
        private async Task<string> ExecuteLongWorkAsync(string processName, int repeatTime, int intervalTime, int stableTime)
        {
            var result = await Task.Run(() => DoWork.DoLongTimeWork(processName, repeatTime, intervalTime, stableTime));

            return result;
        }
DoLongTimeWorkers.cs
    class DoLongTimeWorkers
    {
        public string DoLongTimeWork(string processName, int repeatTime, int intervalTime, int stableTime)
        {
            string result;
            for (int i = 1; i <= repeatTime; i++)
            {
                Console.WriteLine($"{processName} {i}/{repeatTime} ===== ");
                Thread.Sleep(intervalTime);
            }
            result = "Completed";
            Thread.Sleep(stableTime);


            return result;
        }

    }

上記のコードを実行するとTask AとBをシリアルに実行したあと、C-Gを同時に、非同期で実行できることが確認できます。

さて、このTestRunner.csとDoLongTimeWorkers.csを使いまわしてWindows Form Applicationに組み込だのですが、Task.WaitAll実行後処理が次に進みません。
ステップ実行する限り、Task C-Gが実行されないように見えます。Windows Form Applicationの場合になにか足りないのでしょうか。または上記コードは間違っていてたまたまConsole Applicationの場合は動いていただけなのでしょうか。

Form1.cs
        private void button1_Click(object sender, EventArgs e)
        {
            var ts = new TestRunner();
            ts.RunTest(this.textBox1);
        }
TestRunner.cs (TextBoxコントロールを渡す以外はConsole Applicationと同じ)
DoLongTimeWorkers.cs (渡されたTextBoxコントロールにConsole.WriteLineの結果を書き込む以外はConsole Applicationと同じ)

もしご存知の方がいらっしゃいましたらよろしくお願いします。

[ □ Tree ] 返信 編集キー/

▲[ 93833 ] / ▼[ 93845 ]
■93834 / 1階層)  Re[1]: async/awaitのWaitAllの挙動について
□投稿者/ Hongliang (957回)-(2020/02/09(Sun) 21:38:28)
一般論として、GUIアプリケーションにおいてWaitを含むコードを実行することは推奨できません。
最終的にはasyncなイベントハンドラでawaitする形を取るようにします。

さて、お書きのコードにおける具体論として。
まずRunTestを呼び出すとUIスレッドでExecuteLongWorkが実行されるわけで、「UIスレッドで時間のかかる処理をしてはいけない」という原則に従っていません。Task.WaitAllも同様です。
Taskを使う場合、入り口まですべてTaskをチェーンすることが望ましいです。
すなわち、
・void RunTest()ではなく、async Task RunTestAsync()に。
・ExecuteLongWork(...)ではなく、await ExecuteLongWorkAsync(...)に。
・Task.WaitAll(...)ではなく、await Task.WhenAll(...)に。
そして、
・async void button1_Click(object sender, EventArgs e)に。
・ts.RunTest(...)ではなく、await ts.RunTestAsync(...)に。

あとまあ大したことではないですが
var result = await Task.Run(() => ...);
これなら
return Task.Run(() => ...)
で十分です。

最後に、なぜ実行が停止するかの解説を。
まず、RunTest時点では実行スレッドはUIスレッドであり、そこでExecuteLongWorkAsyncを呼び出しています。なのでExecuteLongWorkAsyncもUIスレッドで動作します。
ここでTask.Runした後awaitしていますが、UIスレッドから実行したTaskをawaitすると、await以降の処理はUIスレッドで実行されることになります。
// 実現手段としては、UIスレッドで回っているメッセージループに対してメッセージをPOSTしています。
各ExecuteLongWorkAsyncが開始された後、RunTestはUIスレッドですべてのTaskの完了を待ちます。つまりUIスレッドはここで一旦停止します。
各ExecuteLongWorkAsyncは、Taskが完了した後、await以後の処理(return result)をUIスレッドで実行しようとしますが、Task.WaitAllによってUIスレッドが停止中であるため、いつまでたってもreturn resultを実行できません。
各ExecuteLongWorkAsyncがreturn resultできないので、taskListにAddしたTaskはいつまで経っても完了せず、Task.WaitAllはいつまで経っても終わりません。
このように見事なデッドロックが成立してしまっているのです。
[ 親 93833 / □ Tree ] 返信 編集キー/

▲[ 93834 ] / 返信無し
■93845 / 2階層)  Re[2]: async/awaitのWaitAllの挙動について
□投稿者/ ぱっくん (2回)-(2020/02/10(Mon) 13:48:59)
Hongliangさん、ありがとうございます。具体的なご指導に加えて丁寧な解説までいただき感激しています。
UIスレッドの中で非同期処理をするのには注意が必要とのこと承知しました。

下記のコードで動作するようになりました (非同期処理の理解確認用なのでいろいろツッコミどころはあるのは承知しています)。

Form1.cs
        private async void button1_Click(object sender, EventArgs e)
        {
            var ts = new TestRunner();
            await ts.RunTestAsync(this.textBox1);
        }

TestRunner.cs
        public async Task RunTestAsync(TextBox txtBox)
        {
            ExecuteLongWork("[Single Task] Process A:", 5, 590, 1000, txtBox); // これは一応の動作確認用なのでSingle Taskのままにしています
            ExecuteLongWork("[Single Task] Process B:", 6, 690, 1000, txtBox); // Process AとBをシリアルで実行させ、次にProcess C-Gを同時実行

            var taskList = new List<Task<string>>();

            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process C:", 2, 990, 3000, txtBox));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process D:", 3, 520, 8000, txtBox));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process E:", 3, 800, 1000, txtBox));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process F:", 3, 600, 6000, txtBox));
            taskList.Add(ExecuteLongWorkAsync("[Multi Task] Process G:", 2, 200, 6000, txtBox));

            await Task.WhenAll(taskList.ToArray());
            
        }

DoLongTimeWorkers.cs
        public string DoLongTimeWork(string processName, int repeatTime, int intervalTime, int stableTime, TextBox txtBox)
        {
            string result;
            for (int i = 1; i <= repeatTime; i++)
            {
                txtBox.Parent.Invoke(new Action(() => 
                    {
                        txtBox.Text = txtBox.Text + Environment.NewLine + $"{processName} {i}/{repeatTime} ===== ";
                    }
                ));
                Thread.Sleep(intervalTime);
                
            }
            Thread.Sleep(stableTime);

            result = "Completed";
            return result;
        }

解決済み
[ 親 93833 / □ Tree ] 返信 編集キー/


管理者用

- Child Tree -