Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when multiple parallel threads await the same Task instance which then throws?

Reading through answers to this question prompted me to think about what happens exception-wise when the awaited task throws. Do all "clients" get to observe the exception? I admit I may be confusing a couple of things here; that's the reason I'm asking for clarification.

I'll present a concrete scenario... Let's say I have a server with a global collection of long-running Task instances, started by clients. After starting one or more tasks, a client can query their progress and retrieve results when they become available, as well as any errors that might occur.

Tasks themselves may perform very different business-specific things - normally, no two are quite the same. However, if one of the clients does attempt to start the same task as another had started previously, the server should recognize this and "attach" the second client to the existing task instead of spooling up a new copy.

Now, each time any client queries the status of the task it's interested in, it does something along these lines:

var timeout = Task.Delay(numberOfSecondsUntilClientsPatienceRunsOut);
try
{
    var result = await Task.WhenAny(timeout, task);
    if (result == timeout)
    {
        // SNIP: No result is available yet. Just report the progress so far.
    }

    // SNIP: Report the result.
}
catch (Exception e)
{
    // SNIP: Report the error.
}

In short, it gives the task some reasonable time to finish what it's doing first, then falls back to reporting the on-going progress. This means there's a potentially significant window of time in which multiple clients could observe the same task failing.

My question is: if the task happens to throw during this window, is the exception observed (and handled) by all clients?

like image 894
aoven Avatar asked Jun 26 '17 12:06

aoven


1 Answers

Task.WhenAny will not throw itself. Per the documentation:

The returned task will complete when any of the supplied tasks has completed. The returned task will always end in the RanToCompletion state with its Result set to the first task to complete. This is true even if the first task to complete ended in the Canceled or Faulted state.

You will get back either timeout or task.

If the result is task, and you await that (or get Task.Result), and task has faulted, then that will throw. It doesn't matter how many callers do that, or when they do it -- attempting to get the result of a faulted task always throws. Simple code to demonstrate this:

var t = Task.Run(() => throw new Exception(DateTime.Now.Ticks.ToString()));
try {
    await t;
} catch (Exception e) {
    Console.WriteLine(e.Message);
}
await Task.Delay(1000);
try {
    await t;
} catch (Exception e) {
    Console.WriteLine(e.Message);
}

This will print the same timestamp, twice. The task only runs once, and only has one result, which produces an exception every time you try to get it. If you like you can mix in different threads or parallel calls, this doesn't change the outcome.

Note that in the case of a timeout, there is still the basic possibility of a race condition: two different tasks/threads, both awaiting the same task, may get different results on await Task.WhenAny(timeout, task), based on which task they observe to complete first. In other words, even if await Task.WhenAny(timeout, task) == timeout, task can still have faulted at any point between .WhenAny() deciding it was done and control eventually returning to you. This is to be expected, and your code should handle this (on the next round of waiting, .WhenAny() would return immediately with the faulted task).

like image 151
Jeroen Mostert Avatar answered Nov 14 '22 22:11

Jeroen Mostert