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

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

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

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

複数のタスクで並行して実行させる「重い処理」はこちら↓
```C#:重い処理
        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;
        }
```
１．模範解答01

```C#:模範解答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}");
            }
        }
```
２．模範解答02
```C#:模範解答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();
        }
```