如何使 Task.WaitAll() 在发生任何异常时中断

本文关键字:任何 异常 中断 Task 何使 WaitAll | 更新日期: 2023-09-27 17:56:26

我想让 Task.WaitAll() 在任何正在运行的任务引发异常时爆发,这样我就不必等待 60 秒才能完成。我如何实现这种行为?如果 WaitAll() 无法实现这一点,是否有任何其他 c# 功能或解决方法?

Task task1 = Task.Run(() => throw new InvalidOperationException());
Task task2 = ...
...
try
{
    Task.WaitAll(new Task[]{task1, task2, ...}, TimeSpan.FromSeconds(60));
}
catch (AggregateException)
{
    // If any exception thrown on any of the tasks, break out immediately instead of wait all the way to 60 seconds.
}

如何使 Task.WaitAll() 在发生任何异常时中断

以下内容应该在不更改原始任务代码的情况下执行此操作(未经测试):

static bool WaitAll(Task[] tasks, int timeout, CancellationToken token)
{
    var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
    var proxyTasks = tasks.Select(task => 
        task.ContinueWith(t => {
            if (t.IsFaulted) cts.Cancel();
            return t; 
        }, 
        cts.Token, 
        TaskContinuationOptions.ExecuteSynchronously, 
        TaskScheduler.Current).Unwrap());
    return Task.WaitAll(proxyTasks.ToArray(), timeout, cts.Token);
}

请注意,它只跟踪出错的任务(抛出的任务)。如果还需要跟踪已取消的任务,请进行以下更改:

if (t.IsFaulted || t.IsCancelled) cts.Cancel();

更新后,正如@svick在评论中指出的那样,等待任务代理在这里是多余的。他提出了一个改进版本:https://gist.github.com/svick/9992598。

一种方法是使用 CancelTokenSource。您创建 canceltokensource,并将其作为参数传递给 Task.WaitAll。这个想法是将您的任务包装在 try/catch 块中,如果发生异常,请在 canceltokensource 上调用 cancel。

下面是示例代码

CancellationTokenSource mainCancellationTokenSource = new CancellationTokenSource();
                Task task1 = new Task(() =>
                {
                    try
                    {
                        throw new Exception("Exception message");
                    }
                    catch (Exception ex)
                    {
                        mainCancellationTokenSource.Cancel();
                    }
                }, mainCancellationTokenSource.Token);
                Task task2 = new Task(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(3));
                    Console.WriteLine("Task is running");
                }, mainCancellationTokenSource.Token);
                task1.Start();
                task2.Start();
                Task.WaitAll(new[] { task1, task2}, 
                             6000, // 6 seconds
                             mainCancellationTokenSource.Token
                            );
            }
            catch (Exception ex)
            {   
                // If any exception thrown on any of the tasks, break out immediately instead of wait all the way to 60 seconds.
            }

并行类可以为您完成这项工作。您可以使用 Parallel.For、ForEach 或 Invoke。

using System;
using System.Threading;
using System.Threading.Tasks;
namespace Sample_04_04_2014_01
{
    class Program
    {
        public static void Main(string[] args)
        {
            try
            {
            Parallel.For(0,20, i => {
                            Console.WriteLine(i);
                            if(i == 5)
                                throw new InvalidOperationException();
                            Thread.Sleep(100);
                         });
            }
            catch(AggregateException){}
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }
}

如果其中一个任务引发异常,则不会执行其他任务,但已开始执行的任务除外。ForEach和Invoke正在等待所有任务完成,然后再恢复对调用代码的控制。如果您使用 ParallelLoopState.IsExceptional,您甚至可以进行更精细的控制。Parallel.Invoke 更适合您的情况。

我想

建议对上面Noseratio的出色答案稍作修改。 就我而言,我需要保留抛出的原始异常,并在周围的 try/catch 中区分取消和异常状态。

public static void WaitUnlessFault( Task[] tasks, CancellationToken token )
{
    var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
    foreach ( var task in tasks ) {
        task.ContinueWith(t =>
        {
            if ( t.IsFaulted ) cts.Cancel();
        },
        cts.Token,
        TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Current);
    }
    try {
        Task.WaitAll(tasks, cts.Token);
    }
    catch ( OperationCanceledException ex ) {
        var faultedTaskEx = tasks.Where(t => t.IsFaulted)
            .Select(t => t.Exception)
            .FirstOrDefault();
        if ( faultedTaskEx != null )
            throw faultedTaskEx;
        else
            throw;
    }
}