You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现带超时与未完成任务取消的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:

  1. Shared Cancellation: The mainCts is used to cancel all tasks once any task finishes (successfully or with an error). This ensures no unnecessary bandwidth is wasted on incomplete requests.
  2. Timeout Handling: The timeoutCts triggers after the specified timeout. We link it with mainCts so that canceling either will cancel the linked token, cleaning up the timeout task early if a task completes before the timeout.
  3. Task Creation: Each task is created using the mainCts.Token, which your DownloadUrl method uses to cancel the WebClient request when needed.
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:50:23