实现带超时与未完成任务取消的Task.WhenAny扩展方法
Here's how you can implement the WhenAnyEx extension method to meet your requirements. The core idea is to use a shared cancellation token source to cancel all remaining tasks once any task completes, and handle the timeout by combining it with our cancellation logic:
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; public static class TaskExtensions { public static async Task<Task<TResult>> WhenAnyEx<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, CancellationToken, Task<TResult>> taskFactory, int timeout) { // Main cancellation source to cancel all tasks once we have a winner using var mainCts = new CancellationTokenSource(); // Timeout cancellation source to trigger after the specified delay using var timeoutCts = new CancellationTokenSource(timeout); // Link the two tokens so canceling either cancels the linked token using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(mainCts.Token, timeoutCts.Token); // Create all tasks using the main cancellation token var tasks = source.Select(item => taskFactory(item, mainCts.Token)).ToList(); if (!tasks.Any()) throw new InvalidOperationException("The source sequence contains no elements."); // Create a task that completes when the timeout is reached (or canceled early) var timeoutTask = Task.Delay(Timeout.Infinite, linkedCts.Token); try { // Wait for either any task to complete or the timeout var completedTask = await Task.WhenAny(tasks.Concat(new[] { timeoutTask })); if (completedTask == timeoutTask) { // Timeout occurred: cancel all tasks and throw mainCts.Cancel(); throw new TimeoutException($"No task completed within {timeout} milliseconds."); } // Cancel all remaining tasks since we've found our first completed task mainCts.Cancel(); // Return the completed task (may be successful or faulted) return (Task<TResult>)completedTask; } catch (OperationCanceledException) { // This catches cases where the linked token was canceled before any task completed mainCts.Cancel(); throw new TimeoutException($"No task completed within {timeout} milliseconds."); } } }
Key Details:
- Shared Cancellation: The
mainCtsis used to cancel all tasks once any task finishes (successfully or with an error). This ensures no unnecessary bandwidth is wasted on incomplete requests. - Timeout Handling: The
timeoutCtstriggers after the specified timeout. We link it withmainCtsso that canceling either will cancel the linked token, cleaning up the timeout task early if a task completes before the timeout. - Task Creation: Each task is created using the
mainCts.Token, which yourDownloadUrlmethod uses to cancel the WebClient request when needed. - Return Behavior: Just like
Task.WhenAny, this method returns the first completed task—whether it's successful or faulted. The remaining tasks are canceled immediately.
How to Use It:
You can integrate this with your existing code like this:
var urls = new string[] { "https://stackoverflow.com", "https://superuser.com", "https://www.reddit.com/r/chess" }; try { var firstCompletedTask = await urls.WhenAnyEx(DownloadUrl, 5000); // 5-second timeout var result = await firstCompletedTask; // Unwrap the result (or catch exceptions here) Console.WriteLine($"First Completed Url: {result.Url}"); Console.WriteLine($"Data: {result.Data.Length:#,0} chars"); } catch (TimeoutException) { Console.WriteLine("All tasks timed out."); } catch (Exception ex) { Console.WriteLine($"First completed task failed: {ex.Message}"); }
This implementation ensures that as soon as one request finishes, all others are canceled, and you get the result (or error) from the first completed task. If no tasks finish within the timeout, all are canceled and a TimeoutException is thrown.
内容的提问来源于stack exchange,提问作者Theodor Zoulias
相关产品推荐
相关产品推荐

