web-dev-qa-db-ja.com

例外が発生した場合にTask.WaitAll()を中断させる方法は?

実行中のタスクのいずれかが例外をスローした場合に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.
}
32
user926958

以下は、元のタスクのコードを変更せずにそれを行う必要があります(テストされていません):

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

Updated、コメントで@svickが指摘しているように、タスクプロキシの待機はここでは冗長です。彼は改善されたバージョンを提案します: https://Gist.github.com/svick/9992598

18
noseratio

並列クラスはあなたのために仕事をすることができます。 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);
        }
    }
}

これらのタスクの1つが例外をスローした場合、実行がすでに開始されているタスクを除いて、他のタスクは実行されません。 For、ForEachとInvokeは、すべてのタスクが完了するのを待ってから、呼び出し元のコードへの制御を再開します。 ParallelLoopState.IsExceptionalを使用すると、さらに細かい制御を行うことができます。 Parallel.Invokeはあなたのケースにより適しています。

1
mircea

これを行う1つの方法は、CancellationTokenSourceを使用することです。 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.
            }
0
Michael

上記の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;
    }
}
0
millejos