【问题标题】:How to make Task.WaitAll() to break if any exception happened?如果发生任何异常,如何使 Task.WaitAll() 中断?
【发布时间】:2014-05-15 23:47:59
【问题描述】:

如果任何正在运行的任务引发异常,我想让 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.
}

【问题讨论】:

  • 一位反对者能解释一下吗?这是一个合理的要求。

标签: c# task-parallel-library task async-await


【解决方案1】:

以下内容应该在不改变原始任务代码的情况下进行(未经测试):

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();

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

【讨论】:

  • 你为什么要等待代理任务?等待原始任务会不会更简单(同时在延续逻辑中保持取消)like this
  • @svick,谢谢你的观点,没有充分的理由。这是一个剩余的,在初始版本中我使用cts = new CancellationTokenSource() 而不是CreateLinkedTokenSource 并将原来的token 传递给Task.Wait,所以它需要代理。我正在更新答案。
  • @Noseratio ,为什么需要TaskContinuationOptions.ExecuteSynchronouslyTaskScheduler.Current?代理任务也需要通过cts.Token吗?
  • @Tarc、ExecuteSynchronouslyTaskScheduler.Current 是为了在相应的ante 任务结束时同步执行延续。令牌被传递给代理任务以允许在前任务结束之前取消它。
【解决方案2】:

一种方法是使用 CancellationTokenSource。您创建cancellationtokensource,并将其作为参数传递给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.
            }

【讨论】:

    【解决方案3】:

    并行课程可以为您完成这项工作。您可以使用 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 更适合您的情况。

    【讨论】:

    • 如果所有任务都不同,并且其中一些是 IO 绑定的,例如HttpClient.GetStringAsync?
    • Parallel.Invoke 可以适应不同的任务。如果耗时过长,调用者可以通过在 ParallelOptions 参数中设置取消标记来取消整个操作。
    • 别说Paralell.For不容易支持timeout(不像Task.WaitAll)。关键是你必须在你的Parallel.For lambda 中做一个阻塞httpClient.GetStringAsync().Wait(token) 调用。这扼杀了并行 I/O 绑定任务的想法。 Paralell.XXX 仅适用于受 CPU 限制的同类工作项。
    • 我同意对于 httpClient.GetStringAsync 和其他返回任务的 API,使用并行方法很麻烦,因为这些方法适合并行执行多个操作。因此,取决于具体情况,等待多个并行执行的任务完成的最佳方式。
    【解决方案4】:

    我想建议对上面 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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-05-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-10
      • 1970-01-01
      • 2013-09-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多