Task.WaitAll hang when one task throws and another

2019-07-26 04:26发布

问题:

What should be expected from Task.WaitAll(t1, t2), when t2's execution syncs with t1, but t1 throws an exception before the point it syncs to t2? In this case, it's obvious that t2 will never finish. However since t1 throws an exception, I expected Task.WaitAll(t1,t2) to return with aggregated exception indicating one of the task fails.

However this is not the case. It turns out that Task.WaitAll hangs waiting forever.

I can argue for this behavior that Task.WaitAll does what it claims to wait for ALL tasks to come back, EVEN if one of them already threw an exception. Although I don't prefer it, it is still fine to me as long as I'm aware of it.

However, my question is, is there an alternative API to Task.WaitAll, with the behavior of "waiting for all task to finish, unless one of the task threw an exception"? I imagine this is the behavior I would need most of the time.

Edit 1

I originally used TaskCompletionSource<> for the synchronization. But that's immaterial to the point I want to make. So I changed it with a rudimentary polling.

Edit 2

I wrote an F# equivalent program (see below) and found that it actually does have the behavior as I expected. As mentioned above, Task.WhenAll or Task.WaitAll waits for all tasks to complete, even if a subset of them failed. However, Async.Parallel, as the equivalent of WhenAll in F#, fails eagerly when any sub-task fails, as described by the document and tested in the program below:

If all child computations succeed, an array of results is passed to the success continuation. If any child computation raises an exception, then the overall computation will trigger an exception, and cancel the others. The overall computation will respond to cancellation while executing the child computations. If cancelled, the computation will cancel any remaining child computations but will still wait for the other child computations to complete.

Is there a C# equivalence to F#'s Async.Parallel, in that when waiting for all task to finish, it bails and throws whenever a sub-task fails?

Example that I used:

public void Test()
{
    bool t1done = false;

    Task t1 = Task.Run(() =>
    {
        DoThing(); // throw here
        t1done = true;
    });

    Task t2 = Task.Run(async () =>
    {
        // polling if t1 is done.
        // or equivelantly: SpinWait.SpinUntil(() => t1done);
        while (!t1done) await Task.Delay(10);
    });

    // t2 will never finish, but t1 threw. So I expect Task.WaitAll to throw rather than blindly wait
    // for t2 EVEN THOUGH t1 threw already.
    Task.WaitAll(t1, t2); // never returns
    // or this could be an async wait, but still this function would never return:
    // await Task.WhenAll(t1,t2);

    Console.WriteLine("done");
}

void DoThing()
{
    throw new InvalidOperationException("error");
}

F# "Equivalence" which does have the behavior that I expected:

[<EntryPoint>]
let main argv = 
    let mutable ready : bool = false
    let task1 = async {
        failwith "foo"
        ready <- true
    }

    let task2 = async {
        while not ready do do! Async.Sleep 100
    }

    [| task1; task2 |] 
    |> Async.Parallel // Equivelant to Task.WhenAll() - combining task1 and task1 into a single Async, 
                      // but it throws instead of waiting infinately.
    |> Async.RunSynchronously // run task
    |> ignore

    0

回答1:

There is no equivalent API that I am aware of.

I would suggest that you ensure Task t2 can finish! WaitAll() will then throw the exception from t1 as you expect, and it gives you more control over what happens to Task t2.

Assuming your t2 task does a whole heap of other work before it begins polling t1 for completion to benefit from the two tasks running in parallel. How do you know how much of that work it has completed before t1 throws the exception?

WaitAll does not care how the tasks complete, (Cancelled, Faulted or RanToCompletion) only that it is one of those.

There is really no need for the variable t1done, when you can be checking the result of the Task itself:

Task t1 = Task.Run(() =>
{
    throw null;       
});

Task t2 = Task.Run(async () =>
{
    // check whether task 1 is finished yet 
    while (!t1.IsCompleted) await Task.Delay(10);
    if (t1.Status == TaskStatus.RanToCompletion)
    {
        // we know that t1 finished successfully and did not throw any 
        // error.           
    }
    else
    {
        Console.WriteLine("We know that t1 did not run to completion");
    }
});

try
{
    Task.WaitAll(t1, t2); 
    // this will now throw the exception from t1 because t2 can also finish
}
catch (AggregateException)
{

}

// For interest sake, you can now view the status of each task.            
Console.WriteLine(t1.Status);
Console.WriteLine(t2.Status);
Console.WriteLine("Finished");

Now whether this is the best design for your solution or not I cannot say. Have you looked into Task Continuations? Because if t2 does nothing other than wait for t1 to complete, there are better options... but this should answer the question you have asked directly.



回答2:

since t1 throws an exception, I expected Task.WaitAll(t1, t2) to return with aggregated exception indicating one of the task fails

Why do you expect that? WaitAll means to wait, emmm, all of your tasks, which obviously not the case.

Your code contains inconsistency: you're not failing (or cancelling) the task completion source, which you should do:

var ex = new InvalidOperationException("error");
if (shouldThrow)
{
    // SetCanceled 
    // t1done.SetCanceled();

    // or SetException
    // t1done.SetException(ex);

    throw ex;
}
t1done.SetResult(true);

is there an alternative API to Task.WaitAll, with the behavior of "waiting for all task to finish, unless one of the task threw an exception"?

You can do a loop with WaitAny method so you can check the status of all the tasks completed and throw if any exceptions occur.

PS: consider to switch to async methods WhenAll and WhenAny, and free current thread for additional work. Right now the Wait* method will block your thread without doing anything. Also, you should use a cancellation token to be able to cancel all your tasks if one of them fails.

Edit:

If we are talking only about the unhandled exceptions, then use TaskScheduler.UnObservedTaskException event so you can cancel all other tasks being run.