33

I want to make Task.WaitAll() to break out if any of the running tasks throws an exception, so that I don't have to wait for 60 seconds to finish. How do I achieve such behavior? If WaitAll() cannot achieve that, is there any other c# feature or workaround?

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.
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
user926958
  • 9,355
  • 7
  • 28
  • 33

4 Answers4

20

The following should do it without altering the code of the original tasks (untested):

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

Note it only tracks faulted tasks (those which threw). If you need to track cancelled tasks as well, make this change:

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

Updated, waiting on the task proxies is redundant here, as pointed out by @svick in the comments. He proposes an improved version: https://gist.github.com/svick/9992598.

Farhad Jabiyev
  • 26,014
  • 8
  • 72
  • 98
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 4
    Why are you waiting on the proxy tasks? Wouldn't it be simpler to wait on the original tasks (while keeping the cancellation in continuation logic) [like this](https://gist.github.com/svick/9992598)? – svick Apr 05 '14 at 14:19
  • @svick, thanks for the point, no good reason for this. It's a leftover, in the initial version I used `cts = new CancellationTokenSource()` instead of `CreateLinkedTokenSource` and passed the original `token`to `Task.Wait`, so it needed the proxies. I'm updating the answer. – noseratio Apr 06 '14 at 00:21
  • @Noseratio , why do you need `TaskContinuationOptions.ExecuteSynchronously` and `TaskScheduler.Current`? Also is it necessary for the proxy tasks to be passed `cts.Token`? – Tarc May 09 '16 at 18:52
  • 1
    @Tarc, `ExecuteSynchronously` and `TaskScheduler.Current` is there to make continuations execute synchronously when the corresponding ante task end. Token gets passed to the proxy task to allow cancelling it even before the ante task ends. – noseratio May 09 '16 at 20:58
0

One way of doing that is to use CancellationTokenSource. You create cancellationtokensource, and pass it as an argument to Task.WaitAll. The idea is to wrap your task in try/catch block, and in case of exception, call cancel on cancellationtokensource.

Here's sample code

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.
            }
Michael
  • 2,961
  • 2
  • 28
  • 54
0

Parallel class can do the job for you. You can use Parallel.For, ForEach or 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);
        }
    }
}

If one of these tasks throws an exception then no other task will be executed excepting those whose execution was already started. For, ForEach and Invoke are waiting for all tasks to complete before to resume control to the calling code. You can have even a finer grain control if you use ParallelLoopState.IsExceptional. Parallel.Invoke is more suited for your case.

mircea
  • 493
  • 3
  • 8
  • Does it help if all tasks are different, and some of them are IO-bound, e.g. `HttpClient.GetStringAsync`? – noseratio Apr 04 '14 at 03:06
  • Parallel.Invoke can accommodate different tasks. The caller can cancel the entire operation if it takes too long by setting the cancellation token in ParallelOptions argument. – mircea Apr 04 '14 at 03:58
  • 1
    Let alone `Paralell.For` doesn't easily support `timeout` (unlike `Task.WaitAll`). The point is you'd have to do something like a blocking `httpClient.GetStringAsync().Wait(token)` call inside your `Parallel.For` lambda. Which kills the idea of having parallel I/O-bound tasks. `Paralell.XXX` is only good for CPU-bound, homogeneous work items. – noseratio Apr 04 '14 at 04:08
  • I agree that for httpClient.GetStringAsync and for other APIs that return Tasks it is cumbersome to use Parallel methods as these are suited to perform several Actions in parallel. So it depends from case to case which is best way to wait for several parallel executed tasks to complete. – mircea Apr 04 '14 at 11:38
0

I wanted to suggest a slight modification to Noseratio's excellent answer above. In my case I needed to preserve the original exception thrown, and in a surrounding try/catch distinguish between cancelled and exception states.

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;
    }
}
millejos
  • 301
  • 3
  • 14