🛠️

Task全然わからない

2023/11/11に公開

はじめに

仕事で非同期処理扱う必要があって色々やっているけどこけまくっているので覚えとして残しとく。

一番初めに書いたコード

private void RunTask()
{
	List<Task> tasks = new List<Task>();
	for (int i = 0; i < 5; i++)
	{
		Task t0 = Task.Run(() =>
                {
                    heavyTaskA();
                    strMsgA[i] = $"TaskA {i + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsg[i])));
                });
                tasks.Add(t0);
		
		Task t1 = Task.Run(() =>
                {
                    heavyTaskB();
                    strMsg[i] = $"TaskB {i + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsg[i])));
                });
		tasks.Add(t1);
	}
	Task.WhenAll(tasks);
	textBox1.AppendText("All Tasks Finished.\r\n");
}

実行すると、

this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsg[i])));
型 'System.IndexOutOfRangeException' の例外が Test_Asynchronous.exe で発生しましたが、ユーザー コード内ではハンドルされませんでした
インデックスが配列の境界外です。

エラー出ました。
こういうことらしいです。
BC42324:ラムダ式内で繰り返し変数を使用すると、予期しない結果が発生する可能性があります。
タスク実行する前にループ終わっちゃうから、タスク実行するときに当初の想定と違う値を指してしまいエラー発生、ということのようです。

コードを修正

private void RunTask()
{
	List<Task> tasks = new List<Task>();
	for (int i = 0; i < 5; i++)
	{
		var x = i; // forループに使っている変数を別の変数に代入
		Task t0 = Task.Run(() =>
                {
                    heavyTaskA();
                    strMsgA[x] = $"TaskA {x + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsgA[x])));
                });
                tasks.Add(t0);
		
		Task t1 = Task.Run(() =>
                {
                    heavyTaskB();
                    strMsgB[x] = $"TaskB {x + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsgB[x])));
                });
		tasks.Add(t1);
	}
	Task.WhenAll(tasks);
	textBox1.AppendText("All Tasks Finished.\r\n");
}

再度実行

All Tasks Finished.
TaskA 01 finished
TaskA 02 finished
TaskA 04 finished
TaskA 03 finished
TaskB 02 finished
TaskB 01 finished
TaskB 04 finished
TaskB 03 finished
TaskA 05 finished
TaskB 05 finished

全然待ってくれてない・・・なんで?
Task.WhenAllはTaskを返すのでその返ってきたTaskを待たないといけないようです。
Task.WhenAll メソッド
なのでTask.WhenAllを待つようにしましょう。

private void RunTask()
{
	List<Task> tasks = new List<Task>();
	for (int i = 0; i < 5; i++)
	{
		var x = i; // forループに使っている変数を別の変数に代入
		Task t0 = Task.Run(() =>
                {
                    heavyTaskA();
                    strMsgA[x] = $"TaskA {x + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsgA[x])));
                });
                tasks.Add(t0);
		
		Task t1 = Task.Run(() =>
                {
                    heavyTaskB();
                    strMsgB[x] = $"TaskB {x + 1:00} finished\r\n";
                    this.Invoke((MethodInvoker)(() => textBox1.AppendText(strMsgB[x])));
                });
		tasks.Add(t1);
	}
	await Task.WhenAll(tasks);
	textBox1.AppendText("All Tasks Finished.\r\n");
}

実行結果

TaskB 01 finished
TaskB 05 finished
TaskB 02 finished
TaskA 05 finished
TaskA 03 finished
TaskB 04 finished
TaskA 04 finished
TaskA 02 finished
TaskB 03 finished
TaskA 01 finished
All Tasks Finished.

ちゃんと待てました、良かった。

Discussion