質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.32%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

3回答

726閲覧

C#の非同期処理で最初にOKを確認したら残りをキャンセルしたい。

ohikazuma

総合スコア17

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

2グッド

4クリップ

投稿2025/03/17 01:34

実現したいこと

【実現したいこと】

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

のですが…
await Task.WhenAny(taskA, taskB, taskC);
はタスクが一つでも終われば、次に進みますし、
await Task.WhenAll(taskA, taskB, taskC);
で待てば確実ですが、一番遅いタスクに引っ張れます。
(正常動作するプログラムも記載しています。)

発生している問題・分からないこと

他の待ち方あるでしょうか?

該当のソースコード

C#

1//正常動作するプログラムです。↓ 2 private void button1_Click(object sender, EventArgs e) 3 { 4 非同期処理Async(); 5 } 6 7 private async void 非同期処理Async() 8 { 9 //キャンセルトークン設定 10 var cts = new CancellationTokenSource(); 11 var token = cts.Token; 12 13 Task<string> taskA = Task.Run(() => _重い処理(5, "1st", token), token); //処理時間5秒 14 Task<string> taskB = Task.Run(() => _重い処理(3, "2nd", token), token); //処理時間3秒 15 Task<string> taskC = Task.Run(() => _重い処理(1, "3rd", token), token); //処理時間1秒 16 17 Console.WriteLine("taskA.Status.ToString();" + taskA.Status.ToString()); 18 Console.WriteLine("taskB.Status.ToString();" + taskB.Status.ToString()); 19 Console.WriteLine("taskC.Status.ToString();" + taskC.Status.ToString()); 20 21 //ひとつでも戻ってきたら終了 22 await Task.WhenAny(taskA, taskB, taskC); 23 24 //残りのTaskをキャンセルする。 25 cts.Cancel(); 26 27 //まとめ 28 Console.WriteLine("taskA:" + taskA.Result); 29 Console.WriteLine("taskB:" + taskB.Result); 30 Console.WriteLine("taskC:" + taskC.Result); 31 } 32 33 34 public string _重い処理(int count, string ID, CancellationToken token) 35 { 36 DateTime now = DateTime.Now; 37 Console.WriteLine("ID:" + ID.ToString() + " ⇒入りました。" + now.ToString() + "/" + now.Millisecond); 38 39 //長大処理の本体(指定秒停止する) 40 for (int i = 0; i <= count - 1; i++) 41 { 42 if (token.IsCancellationRequested) 43 { 44 Console.WriteLine(ID.ToString() + "/キャンセル検知!"); 45 return ID + "/キャンセルされました!"; 46 } 47 48 //長い処理 49 Thread.Sleep(1000); 50 } 51 52 Console.WriteLine(ID + " / 処理完了:" + count.ToString() + "秒間停止しました。"); 53 54 string ans = ""; 55 56 if (ID == "1st") //1stは5秒で完了する。 57 { 58 ans = ID + "/NG"; 59 } 60 else if (ID == "2nd") //2ndは3秒で完了する。 61 { 62 ans = ID + "/OK"; //←ここだけOK!これを待ちたい。 63 } 64 else if (ID == "3rd") //3rdは1秒で完了する。 65 { 66 ans = ID + "/NG"; 67 } 68 69 return ans; 70 }

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

//ダメだと判りつつ…やっぱりダメなコード

await Task.WhenAny(taskA, taskB, taskC); Console.WriteLine("1回目-------------"); if (taskA.Result.Contains("OK") || taskB.Result.Contains("OK") || taskC.Result.Contains("OK")) { Console.WriteLine("taskA:" + taskA.Result); Console.WriteLine("taskB:" + taskB.Result); Console.WriteLine("taskC:" + taskC.Result); cts.Cancel(); goto SkipPoint; } await Task.WhenAny(taskA, taskB, taskC); Console.WriteLine("2回目-------------"); if (taskA.Result.Contains("OK") || taskB.Result.Contains("OK") || taskC.Result.Contains("OK")) { Console.WriteLine("taskA:" + taskA.Result); Console.WriteLine("taskB:" + taskB.Result); Console.WriteLine("taskC:" + taskC.Result); cts.Cancel(); goto SkipPoint; } await Task.WhenAny(taskA, taskB, taskC); Console.WriteLine("3回目-------------"); if (taskA.Result.Contains("OK") || taskB.Result.Contains("OK") || taskC.Result.Contains("OK")) { Console.WriteLine("taskA:" + taskA.Result); Console.WriteLine("taskB:" + taskB.Result); Console.WriteLine("taskC:" + taskC.Result); cts.Cancel(); goto SkipPoint; } SkipPoint:

補足

特になし

TN8001, melian👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

こんにちは。

ひとまず以下のようなメソッドを作っておけば目的は達成できそうです。

csharp

1private static async Task<T> CancelAfter<T>(Func<CancellationToken, Task<T>> task, CancellationTokenSource cancellation) 2{ 3 var result = await task(cancellation.Token).ConfigureAwait(false); 4 await cancellation.CancelAsync().ConfigureAwait(false); 5 return result; 6}

csharp

1var cts = new CancellationTokenSource(); 2 3// どれかの処理が完了した時点で CancellationTokenSource が Cancel になる 4var task = await Task.WhenAny( 5 CancelAfter(token => TaskA(token), cts), 6 CancelAfter(token => TaskB(token), cts), 7 CancelAfter(token => TaskC(token), cts) 8); 9 10// どれか一つが完了した時点で Result が取れる 11var result = task.Result;

仮に各タスクが失敗で終わる可能性があるとこれだけだとうまくいかないですが、そうでなければ必要十分かと思います。


追記:
各 Task が完了 (正常終了) した上で戻り値が OK 以外になる場合があると上記の処理では対応できないので、
予め各 Task が OK 以外の場合に例外を返すようにしておくことで、以下のようなヘルパー関数で対応できるようになるかと思います。

csharp

1private static Task<T> WhenAnySucceeded<T>(params IEnumerable<Func<CancellationToken, Task<T>>> tasks) 2{ 3 var tcs = new TaskCompletionSource<T>(); 4 var cts = new CancellationTokenSource(); 5 6 _ = Task.Run(async () => 7 { 8 try 9 { 10 await Task.WhenAll(tasks.Select(async task => 11 { 12 var result = await task(cts.Token).ConfigureAwait(false); 13 cts.Cancel(); 14 tcs.TrySetResult(result); 15 })).ConfigureAwait(false); 16 } 17 catch (TaskCanceledException) 18 { 19 tcs.TrySetCanceled(); 20 } 21 catch (Exception exception) 22 { 23 tcs.TrySetException(exception); 24 } 25 }); 26 27 return tcs.Task; 28}

csharp

1// 最初に成功した (値を返した) Task の結果を得る 2var result = await WhenAnySucceeded( 3 token => TaskA(token), 4 token => TaskB(token), 5 token => TaskC(token)); 6 7... 8 9async Task<string> TaskA(CancellationToken token) 10{ 11 ... 12 if (status == "OK") // status が OK でない場合は例外を発生させることで Task を失敗にする 13 return result; 14 else 15 throw new Exception("TaskA failed"); 16}

投稿2025/03/17 03:21

編集2025/03/17 08:35
tamoto

総合スコア4260

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ohikazuma

2025/03/17 03:44

早速のご回答ありがとうございます。 読解します!
guest

0

TaskCompletionSourceクラスを使用します

cs

1var tcs = new TaskCompletionSource<string>(); // 最初にOKを見つけた時点でタスクを解決するため

最終的に、ここに解決/拒否を通知します。
通知を待機します。

cs

1await tcs.Task;

その間に、非同期で処理を実施し任意の条件で解決(SetResult)します。

オンラインコンパイラで動作を確認しました。
ほとんどAIに書いてもらいました。

cs

1using System; 2using System.Threading; 3using System.Threading.Tasks; 4using System.Collections.Generic; 5 6 7class Program 8{ 9 static async Task Main(string[] args) 10 { 11 await 非同期処理Async(); 12 } 13 14 private static async Task 非同期処理Async() 15 { 16 //キャンセルトークン設定 17 var cts = new CancellationTokenSource(); 18 var token = cts.Token; 19 20 Task<string> taskA = Task.Run(() => _重い処理(5, "1st", token), token); //5秒 21 Task<string> taskB = Task.Run(() => _重い処理(3, "2nd", token), token); //3秒 22 Task<string> taskC = Task.Run(() => _重い処理(1, "3rd", token), token); //1秒 23 24 Console.WriteLine($"taskA: {taskA.Status}"); 25 Console.WriteLine($"taskB: {taskB.Status}"); 26 Console.WriteLine($"taskC: {taskC.Status}"); 27 28 // タスクを実行する **今回のキモです。汎用処理なので関数化するなり改変するなりしてください。 29 await Task.Run(async () => 30 { 31 var tasks = new List<Task<string>> { taskA, taskB, taskC }; 32 var count = tasks.Count; 33 var tcs = new TaskCompletionSource<string>(); // 最初にOKを見つけた時点でタスクを解決するため 34 35 // 各タスクを並行して実行 36 foreach (var t in tasks) 37 { 38 // タスクごとに処理 39 _ = Task.Run(async () => 40 { 41 var result = await t; 42 count = count - 1; 43 44 // 結果に"OK"が含まれていた場合、即時終了 45 if (result.Contains("OK")) 46 { 47 tcs.SetResult(result); // 結果を返す 48 } 49 50 // すべてのタスクが終了したらcountが0になる 51 if (count <= 0 && !tcs.Task.IsCompleted) 52 { 53 tcs.SetResult("No OK found."); 54 } 55 }); 56 } 57 58 // 結果を待つ 59 return await tcs.Task; 60 }); 61 62 63 // 残りをキャンセル 64 cts.Cancel(); 65 66 // それぞれの結果取得 67 try 68 { 69 Console.WriteLine($"taskA: {taskA.Result}"); 70 } 71 catch (AggregateException e) 72 { 73 Console.WriteLine($"taskA: {e.InnerException.Message}"); 74 } 75 76 try 77 { 78 Console.WriteLine($"taskB: {taskB.Result}"); 79 } 80 catch (AggregateException e) 81 { 82 Console.WriteLine($"taskB: {e.InnerException.Message}"); 83 } 84 85 try 86 { 87 Console.WriteLine($"taskC: {taskC.Result}"); 88 } 89 catch (AggregateException e) 90 { 91 Console.WriteLine($"taskC: {e.InnerException.Message}"); 92 } 93 } 94 95 public static string _重い処理(int count, string ID, CancellationToken token) 96 { 97 DateTime now = DateTime.Now; 98 Console.WriteLine($"ID: {ID} ⇒ 開始 {now:HH:mm:ss.fff}"); 99 100 //長大処理 101 for (int i = 0; i < count; i++) 102 { 103 if (token.IsCancellationRequested) 104 { 105 Console.WriteLine($"{ID}: キャンセル検知!"); 106 return $"{ID} / キャンセルされました"; 107 } 108 Thread.Sleep(1000); //1秒停止 109 } 110 111 Console.WriteLine($"{ID}: 処理完了({count}秒)"); 112 113 // 結果 114 return ID switch 115 { 116 "1st" => $"{ID}/NG", 117 "2nd" => $"{ID}/OK", // これがOK 118 "3rd" => $"{ID}/NG", 119 _ => $"{ID}/Unknown" 120 }; 121 } 122} 123

蛇足。
解決や拒否の考え方というかクラスが色々あってC#は大変ですね。
考え方としてはTaskをTaskでラップして、任意のタイミングで解決や拒否を投げるという考え方です。
returnでしか解決できないのかと困ってましたが、TaskCompletionSourceというクラスがあって助かりました。

投稿2025/03/17 14:43

utm.

総合スコア647

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ohikazuma

2025/03/18 06:41

utm.先生、できました。 肝の部分、明日じっくり読込みます。(^ω^)
utm.

2025/03/18 09:40

どのような方法で解決したのかを自己回答することで、同じ問題を抱えているほかのユーザーが助かるかも知れませんので、締め切る際に記載することをおすすめします
tamoto

2025/03/18 10:49

本回答のコードは正しい考え方で作られたコードですが、レースコンディションとデッドロックが稀に発生する実装になっているため実用する場合は修正が必要と思います。
utm.

2025/03/18 11:28

おっしゃる通りだと思います。AIが事前にコード生成時に防げなかったのは謎ですが、 TrySetResultを使用して成功を通知し、Integerがスレッドセーフでは無いようなのでlockをする必要があります。 C#について詳しくありませんが、その他に問題がございましたら訂正して頂けると幸いです。 また、質問に回答しようと思った頃には知りませんでしたが、回答する直前にはこれが並列処理だということを知っていたのに、留意しなかった私の過失です。 お詫びして訂正させていただきます。
ohikazuma

2025/03/24 02:30

tamoto先生、utm.先生 質問にご回答頂きありあがとうございました。 宇宙語を聞いている様ですが… その後もじっくりコードを読込んでます。 ところで、 // 結果を待つ return await tcs.Task; の"return"は、 非同期処理Async() の"return"ではなく(このメソッドが終わってしまう様に見えます)、 await Task.Run 用の"return"なのでしょうか???
utm.

2025/03/24 02:40

29行目のTask.Runは関数を引数にとるのでその関数内の戻り値としてreturnを指定しています。 コールバック関数と、無名関数(ラムダ関数)を組み合わせています。 考え方を示しただけですのでご自身で改良できないのであれば、上記ソースは可能であれば使わないことをおすすめします。 宇宙語に感じるかもしれませんが、スレッドセーフではない型(Integer)の使用や、SetResultの使用が含まれます。
ohikazuma

2025/03/24 02:49

現在の能力ではこのプログラムを改良できないので、参考とさせて頂きます。 とても勉強になります。 ありがとうございました!
utm.

2025/03/24 02:49

うーん。考え方的にほかの回答より自分のやり方が正しいと思うんだが、受け入れられないのか...笑
ohikazuma

2025/03/24 02:55

「レースコンディションとデッドロックが稀に発生する実装になっている」のを回避する技術がないので… 申し訳ありません。
tamoto

2025/03/24 03:50

「複数の Task を競争させる」という元の要件に沿うものはこの回答のコードになりますが、そもそも C# の Task という非同期フレームワークでは Task の失敗は例外で表現される仕様になっているので、戻り値をさらに検証するという要件自体が Task のフレームワークとは合致していない点が問題です。現在の仕様から、検証処理を Task 内に埋め込み失敗を例外扱いにできるのであれば、自分の追記のコードが C# の Task フレームワークに乗せながら最も一般化された形になると思います。
utm.

2025/03/24 04:50 編集

tamotoさん えぇ...?本当ですか? 例外で表現されるというのはSetExceptionにExceptionクラスを渡すという意味ではなくthrowするという意味ですよね? 例外駆動プログラミング(例外によるプログラミング)とも呼ばれると思いますがそれらは設計によるのでは? TaskをラップするのがTask APIに反しているのであればそもそもTaskを使う場面がない気がします。 (なぜなら、TaskをラップしていようとしていまいとひとつのTaskであることになんの変わりもないはずですから) もしもいくつかの既存の非同期処理がエラーを返すことで失敗を表現していると言いたいのであればそれは納得しますが、だからといってTaskのような非同期APIがそれを求めた設計であるとしているならば飛躍だと感じます。 根本的にTaskを定義するのも、私がやったようにTaskをラップするのもTaskの概念としては同じだと思うのですが、C#でこれがおかしいとなると線引きはどこにあるんでしょう?いささか不思議な指摘です。 最後に改めて読んでみましたが意味が全然分からないです。 > そもそも C# の Task という非同期フレームワークでは Task の失敗は例外で表現される仕様になっているので、戻り値をさらに検証するという要件自体が Task のフレームワークとは合致していない点が問題です。 何が失敗で何がエラーかはプログラマが自由に決められるものでは? どこかにエラーという集合があって今回のケースはそれに内包されていたとでも言いたいのでしょうか。 例えばあなたの言っていることが正しければ、 Task.WhenAnyはどのような実装になっているのですか? なぜこれは初めに失敗したtaskがある際にエラーがthrowされないのですか? ifの正常ケースでの分岐はダメで、try-catchを使えばセーフなのですか? あなたと私のコードの違いはそこだけですよね。 それって完全に好みだと思うのですが。バリデーションでエラーをthrowする設計もあれば、正常処理としてifで処理するという設計もあるというのが自分の認識です。 今回の私のコードは簡単のためにIntegerをsetResultしてますがこれがTaskになっているだけの話ですよね? 何か読みが間違っていますか? 【ユースケース】 例えば特定のWebAPIを使い複数ネットワーク通信をして特定の値を含んでいればそれを使うとしたい時、あなたのように一つ一つのTaskでラップしてthrowするのが適切ですか? 私のようにひとつの処理でどんなTaskが来るかに依存しない設計は邪道ですか? C#でネットワーク通信がどのようなオブジェクトで表現されるのか知りませんが、どこでラップするかの違いでしかないと思うのですけど...
ohikazuma

2025/03/24 04:57

tamoto先生 ありがとうございます。 自分の知識不足が判りました。 勉強します。
utm.

2025/03/24 05:25

ohikazumaさん 高度な処理をしたい訳では無いのであれば勉強の必要はないかと思います。 汎用的なやり方ですので、処理の"考え方"は覚えておくと良いかと思います。
tamoto

2025/03/24 07:08

utm. さん > SetException ではなく throw する そのような意図ではありませんでした。 まず、本回答のコードのように、taskABC が string を戻り値に持ち、その戻り値が "OK" かどうかで成功か失敗を判定する、というのは、「Task としては全て成功で終了している」ということを意味します。 Task のみを抽象的に扱う際に「最初に成功した Task を取る」という機能を実装する場合、戻り値が OK でない Task というのは、すなわち「失敗した Task」である必要があります。 C# の Task は JavaScript の Promise みたいなものですが、Promise でいう resolve と reject の2つの状態を「正常に完了した」「例外で終了した」の2種類で扱います。 そのため、本ユーティリティ関数に渡す各 Task (taskABC) が、Task レベルで「成功」「失敗」を持っていれば、Task フレームワーク上で「最初に成功した」を扱うことができるようになるわけです。 そのために、taskABC は「"OK" ではない戻り値」を返す代わりに、例外を出して Task を reject 状態にした方が良い、ということを言っています。 TaskCompletionSource は Task ではないもの (もちろんそれが Task でもよい) から新しい Task を作り上げるものなので、その Task を失敗状態にするために SetException を使います。 その場合は Task 内部に失敗を持たせるための SetException なので、throw することはありません。
utm.

2025/03/24 07:44

tamotoさん 例外を出すというのはthrowすると同義な気がしますが、 仰る内容については理解しているつもりです。 色々認識の違いがある気がしますが、返信くださったことで明確に感じました。 >{ まず、本回答のコードのように、taskABC が string を戻り値に持ち、その戻り値が "OK" かどうかで成功か失敗を判定する、というのは、「Task としては全て成功で終了している」ということを意味します。 Task のみを抽象的に扱う際に「最初に成功した Task を取る」という機能を実装する場合、戻り値が OK でない Task というのは、すなわち「失敗した Task」である必要があります。 } これはあなたがそう規定しているという話かと思いますが、 その場合こちらとしては疑問は特にありません。 私は純粋にそうじゃないだろうと思っているという話です。
tamoto

2025/03/24 08:16

utm. さん Task に対して何らかの処理を行うユーティリティを定義するのであれば、Task の仕組みに正しく乗ったより汎用的なものが作れますよ、という提案に過ぎないので、本回答のコードに問題があると言っているわけではありません。 単純に、「最初に成功した一つを取りたい」という要件を自然に満たしたいとき、成功したもの以外は「失敗」していて欲しいと考えるのは自然だと思いませんでしょうか。 Task 単体の視点においては、戻り値が返ったものは「成功したもの」という扱いである、ということが言いたかっただけです。
utm.

2025/03/24 09:03

tamotoさん ご返信ありがとうございます。 おそらく成功や失敗の考え方が違うのだと思います。 エラーを駆使してプログラムを書くかどうかは、好みや設計の問題だと私は思っていて、意図した結果は例外では無いという考えに私は同意していますので、例外を使ったプログラミングを使う/使わないというところですれ違いが起きているのだと思います。 (関係ないですがvisual Studioってデバッグで実行するとIDEの機能としてexceptionが出た時にデバッガが起動されてcatchされるまでステップ実行できるみたいなオプションありませんでしたっけ) それと、もうひとつすれ違っている原因としては質問にある _重い処理 という関数が、何らかのAPIを質問するにあたって差し替えたものだと私が先入観を持って回答している点だと思います。 複数のWebAPI(例えばDBサーバーにデータを挿入する/Fetchすると考えてもいい)を実行する際に、開発者側で特定の条件を満たせばほかは無視できると考えたならば、1つ1つasync Task<string> TaskA(CancellationToken token) のような関数でラップするよりは楽だろうなぁという感じです。 でも、改めて考えてみると1つ1つ処理を取りだして検証した方がテストはしやすいかも知れません。 うーん。でも、1つ1つ処理を出すとスレッドの指定がその関数に依存するなぁ。C#ではあまり考えないのだろうか。それとも、ネットワーク処理とか処理を書く際に明らかだからまあそこはいいのだろうか。 まあそもそもスレッドを行き来するような非同期関数があるならクラス設計がダメか。ラップしてAPIを実行する際のスレッド切り替えは気にしなくていいな。 考えが知れてよかったです。 話しぶりからするにwhenAllを使うことを前提としているような感じもずっとしていますが、そういうセオリーがあるのかもくらいに思っています。
tamoto

2025/03/24 09:53

utm. さん 「例外で処理フローを構築する」については明らかに知られたバッドプラクティスなので避けるべきは間違いないです。 キーポイントになっているのは、「Task が失敗となるのはその処理が例外で終了したとき」という Task の仕様に関する点です。 今回のやり取りの中で問題と考えていたところはただ一つで、「戻り値 "NG" を返して終了した Task のステータスは Succeeded である」という点です。(実際の Status プロパティの名称は少し違いますが) taskABC の中身が何であろうと、それを単なる Task の一つとして見たときに、それは「正常に完了した Task」になってしまうわけです。 実は C# の Task の「例外で失敗を表す」という設計は個人的には全く好きではないのですが (先の例外フロー的発想でキモい)、既存の Task フレームワークに乗るためにはその設計に合わせる必要があったわけですね。 ただし、そもそもフレームワークに正しく乗せることは推奨であっても必須ではないので、その中で自分はフレームワークに乗せる方を選びたいというスタンスの発言になりますね。
utm.

2025/03/24 23:29 編集

詳しいご説明ありがとうございます。 私的には戻り値NGを返して終了したTaskのステータスがSucceededでも特に気にしません。 後続の処理でTaskをオブジェクトとして、どこかのタイミングで成功したか失敗したかを知りたいみたいな話なんでしょうか。それは考慮していませんでした。 仮に、任意の結果を得られたのでリソースを解放したいのでExceptionを発生させるという話であれば、私なら他の方法を探すと思います。 【以外蛇足】 例外で失敗を表すことに関しては、 例外が発生したら正常処理として進行して欲しくないからエラーをthrowするし、 WhenAllなどではTaskオブジェクトの状態を抽象化して後続の処理を実行して欲しいという意図があるのかもしれません。 非同期処理の中で起きたエラーが失敗として表されるのはエラーの機構が存在する理由からして自然な発想にも思えます。 (プログラムからすると予期せぬエラーはどうすればいいのかわからない状態で、勝手に無視して後続処理を進める訳にも行かない) 戻り値NGを返して終了したTaskのステータスがSucceededでも特に気にしないことに関しては、私はそれがそう言うAPIだと思っているからだと思います。 あくまで例ですが郵便局のAPIを使用して返してくるjsonから「東京」を含む文字だけ取得したい(ほかは無視したい)、 と言った場合、郵便局からしたら東京以外の文字列はエラーかと言われれば違いますし、内部でわざわざそれをエラーとする必要も無いと考えます。 既存のTaskフレームワークに乗るというのが何を指しているのか分かりませんが、自分ならバリデーションを実行するhttpサーバーがあってもバリデーションエラー発生時は402エラーではなく200statusを返します。 もし私がAPIやモジュールを作成しているのならば、その限りではありませんので、エラーを返すかも知れませんが、 今のところこの文脈はそういう話ではないと認識しています。 Taskのエラーと失敗に関してもうひとつ言えるのは Taskは関数ではなく、非同期処理の「状態」をあくまで抽象化した概念なので、特定のTaskの中でエラーがthrowされると、唐突にプログラムがエラーの状態になるということではなく(スレッドが宙に浮いている形ではなく)、何らかのタイミングまで(例えばawaitのタイミング、awaitした時に実行されるのであれば実行のタイミングと解釈しても良い)、Taskがthrowされたエラーを保持し続ける、そのために失敗という概念が存在すると考えても良いと考えています。 一般的な非同期処理の概念についての話なのでC#のTaskがこれに則っているかは詳しくありませんし、断言していいものか不明ではありますが、今のところはこの考えで破綻は無いです。
tamoto

2025/03/25 12:18

utm. さん C# の Task も考え方は一般的な非同期処理の概念に則ったもので、その認識で間違いはありません。 今更ですが、自分と意見が衝突していたのは、要件を満たす具体的な実装を前提にしていた点にあったように思いました。 要するに、自分は最初から、抽象化された汎用ユーティリティとしてのシグネチャとなる「複数の Task<T>を取って、最初に成功した T を返す非同期関数」という多相関数を考えてしまっていたため、その部分が意見の違いにつながったのだと思います。 特定の非同期処理が戻り値に "NG" を返す、というそれ自体は悪くなく、そういう設計はもちろん自分もやりますが、先の Task<T> の T にはあらゆる型が入るため、そのような成功失敗などの付加情報を含むことができないので、必然的に「複数の Task<T> の最初に成功したもの」を取るためには、そうでない Task<T> は失敗で終わらなければならない、という順序で思考していました。 その上で、この汎用ユーティリティを本要件で扱う場合、競争させる複数の Task<T> は成功したもの以外失敗させる必要がある、そして Task<T> を失敗させるには例外を出して中断させる必要がある、という方向で話がつながるわけでした。 そして、このようなユースケースに依存しないあらゆる非同期処理に適用可能な一般化された関数が存在していたとき、それを活用できるように利用者のコードの方を合わせることを指して「Task のフレームワークに乗る」と表現していました。 最初から「複数の Task<string> を取って、最初に戻り値 "OK" を返したものだけを扱う」という一つの用途しかない関数を考えるのでしたら例外云々の話は全く関係なくなるので、そこで話が食い違っていたようです。 この辺りの合意を最初に取れていなかったのは申し訳なく思います。
guest

0

ベストアンサー

List等に入れて完了した順に削除しながら、何度もWhenAnyするテクニックがあるようです。
Processing tasks as they complete - .NET Parallel Programming
c# - How to implement Task.WhenAny() with a predicate - Stack Overflow
C#でリストに入れた実行中のTaskを実行が終わったものから取得したい | mrtska.net


Rxを使ってよければいくらかスッキリします。
c# - How to turn a list of Tasks into an Observable and process elements as they are completed? - Stack Overflow

cs

1using System.Diagnostics; 2using System.Reactive.Linq; 3 4namespace Qfh0dnwbeadqiyq; 5 6public partial class Form1 : Form 7{ 8 public Form1() => InitializeComponent(); 9 10 private void Button1_Click(object sender, EventArgs e) => 非同期処理Async(); 11 12 private async void 非同期処理Async() 13 { 14 var cts = new CancellationTokenSource(); 15 var token = cts.Token; 16 17 var taskA = Task.Run(() => _重い処理(5, "1st", token), token); 18 var taskB = Task.Run(() => _重い処理(3, "2nd", token), token); 19 var taskC = Task.Run(() => _重い処理(1, "3rd", token), token); 20 21 Debug.WriteLine($"taskA.Status.ToString();{taskA.Status}"); 22 Debug.WriteLine($"taskB.Status.ToString();{taskB.Status}"); 23 Debug.WriteLine($"taskC.Status.ToString();{taskC.Status}"); 24 25 //await Task.WhenAny(taskA, taskB, taskC); 26 27 var tasks = new List<Task<string>>([taskA, taskB, taskC]); 28 29 while (0 < tasks.Count) 30 { 31 var task = await Task.WhenAny(tasks); 32 var s = await task; 33 if (s.Contains("OK", StringComparison.Ordinal)) break; 34 tasks.Remove(task); 35 } 36 37 //await tasks 38 // .ToObservable() 39 // .Merge() 40 // .Any(x => x.Contains("OK", StringComparison.Ordinal)); 41 42 cts.Cancel(); 43 44 Debug.WriteLine($"taskA:{taskA.Result}"); 45 Debug.WriteLine($"taskB:{taskB.Result}"); 46 Debug.WriteLine($"taskC:{taskC.Result}"); 47 } 48 49 50 public string _重い処理(int count, string id, CancellationToken token) 51 { 52 Debug.WriteLine($"ID:{id} ⇒入りました。{DateTime.Now:o}"); 53 54 for (var i = 0; i < count; i++) 55 { 56 if (token.IsCancellationRequested) 57 { 58 Debug.WriteLine($"{id}/キャンセル検知!"); 59 return $"{id}/キャンセルされました!"; 60 } 61 62 Thread.Sleep(1000); 63 } 64 65 Debug.WriteLine($"{id} / 処理完了:{count}秒間停止しました。"); 66 67 return id switch 68 { 69 "1st" => $"{id}/NG", 70 "2nd" => $"{id}/OK", 71 "3rd" => $"{id}/NG", 72 _ => "", 73 }; 74 } 75}

NuGet Gallery | System.Reactive

投稿2025/03/17 09:41

編集2025/03/24 06:14
TN8001

総合スコア9957

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

TN8001

2025/03/17 09:41

この辺得意でないので何か考慮漏れがあったらすいません^^;
ohikazuma

2025/03/18 07:10

TN8001先生、出来ました! 明日、落ち着いて咀嚼します。 いろんな書き方出来ることに驚きました。
TN8001

2025/03/18 07:36

> 出来ました! よかったです^^ > 明日、落ち着いて咀嚼します。 回答に対する疑問点はお気軽にコメントください。
ohikazuma

2025/03/24 02:33

理解できました!!! こんなことが出来るのですね!!! これを使わさせて頂きます。 ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.32%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問