3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

非同期の待ち方(期待する戻り値を得たら、残りのタスクをキャンセルする)

Last updated at Posted at 2025-03-24

tamoto先生、utm.先生、TN8001先生に教えて頂きました。
ありがとうございます。

[したい事]
1.button1をクリックすると、非同期処理Asyncメソッドが起動され、そこからtaskA、taskB、taskCが非同期で起動されます。
2.3つのタスクが完了した順に戻り値戻り値を確認し、"OK"が含まれていれば残りの動作中タスクをキャンセルしたい。

のですが、
  await Task.WhenAny(taskA, taskB, taskC);
はタスクが一つでも終われば、次に進みますし、
  await Task.WhenAll(taskA, taskB, taskC);
で待てば確実ですが、一番遅いタスクに引っ張れます。どうしましょ?

複数のタスクで並行して実行させる「重い処理」はこちら↓

重い処理
        static string _重い処理(int count, string ID, CancellationToken token)
        {
            DateTime now = DateTime.Now;
            Console.WriteLine($"ID: {ID} ⇒ 開始 {now:HH:mm:ss.fff}");

            //長大処理
            for (int i = 0; i < count; i++)
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine($"{ID}: キャンセル検知!");
                    return $"{ID} / キャンセルされました";
                }
                Thread.Sleep(1000); //1秒停止
            }

            Console.WriteLine($"{ID}: 処理完了({count}秒)");

            string ans = "";

            if (ID == "1st")        //1stは5秒で完了する。
            {
                ans = ID + "/NG";
            }
            else if (ID == "2nd")   //2ndは3秒で完了する。
            {
                ans = ID + "/OK";   //←ここだけOK!これを待ちたい。
            }
            else if (ID == "3rd")   //3rdは1秒で完了する。
            {
                ans = ID + "/NG";
            }
            return ans;
        }

1.模範解答01

模範解答01
        using System.Threading;     //CancellationTokenSource利用の為追加要

        private async void buttonサンプルA_Click(object sender, EventArgs e)
        {
            await 非同期処理01Async();
        }

        private static async Task 非同期処理01Async()
        {
            //キャンセルトークン設定
            var cts = new CancellationTokenSource();
            var token = cts.Token;

            Task<string> taskA = Task.Run(() => _重い処理(10, "1st", token), token); //10秒
            Task<string> taskB = Task.Run(() => _重い処理(5, "2nd", token), token);  //5秒
            Task<string> taskC = Task.Run(() => _重い処理(1, "3rd", token), token);  //1秒

            // タスクを実行する **今回のキモです。汎用処理なので関数化するなり改変するなりしてください。
            await Task.Run(async () =>
            {
                var tasks = new List<Task<string>> { taskA, taskB, taskC };
                var count = tasks.Count;
                var tcs = new TaskCompletionSource<string>(); // 最初にOKを見つけた時点でタスクを解決するため

                // 各タスクを並行して実行
                foreach (var t in tasks)
                {
                    // タスクごとに処理
                    var taskTmp = Task.Run(async () =>
                    {
                        var result = await t;
                        count = count - 1;

                        // 結果に"OK"が含まれていた場合、即時終了
                        if (result.Contains("OK"))
                        {
                            tcs.SetResult(result); // 結果を返す
                        }

                        // すべてのタスクが終了したらcountが0になる
                        if (count <= 0 && !tcs.Task.IsCompleted)
                        {
                            tcs.SetResult("Not Found.");
                        }
                    });
                }
                // 結果を待つ 
                //↓【このreturnについて】Task.Runは関数を引数にとるのでその関数内の戻り値としてreturnを指定している。
                return await tcs.Task;
            });

            // 残りをキャンセル
            cts.Cancel();

            // それぞれの結果取得
            try
            {
                Console.WriteLine($"taskA: {taskA.Result}");
            }
            catch (AggregateException e)
            {
                Console.WriteLine($"taskA: {e.InnerException.Message}");
            }

            try
            {
                Console.WriteLine($"taskB: {taskB.Result}");
            }
            catch (AggregateException e)
            {
                Console.WriteLine($"taskB: {e.InnerException.Message}");
            }

            try
            {
                Console.WriteLine($"taskC: {taskC.Result}");
            }
            catch (AggregateException e)
            {
                Console.WriteLine($"taskC: {e.InnerException.Message}");
            }
        }

2.模範解答02

模範解答02
        using System.Threading;     //CancellationTokenSource利用の為追加要
        
        private void buttonサンプルB_Click(object sender, EventArgs e)
        {
                非同期処理02Async();
        }

        private async void 非同期処理02Async()
        {
            var cts = new CancellationTokenSource();
            var token = cts.Token;

            var taskA = Task.Run(() => _重い処理(10, "1st", token), token);
            var taskB = Task.Run(() => _重い処理(5, "2nd", token), token);
            var taskC = Task.Run(() => _重い処理(1, "3rd", token), token);

            var tasks = new List<Task<string>> { taskA, taskB, taskC };

            while (0 < tasks.Count)
            {
                var task = await Task.WhenAny(tasks);
                var s = await task;
                if (s.Contains("OK"))
                {
                    break;
                };
                tasks.Remove(task);
            }
            cts.Cancel();
        }
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?