简体   繁体   中英

Handling Task Exceptions - custom TaskScheduler

I am writing a custom FIFO-queued thread-limited task scheduler.

  1. I would like to be able to guarantee that unhandled task exceptions bubble up or escalate immediately, and proceed to end the process.

  2. I would also like to be able to Task.Wait() and handle an AggregateException that might pop out, without causing the process to end.

Can these two requirements be met simultaneously? Am I thinking about this the wrong way?


Further information

The crux of the issue is that I don't always want to Task.Wait() on a task to complete.

After executing the task on my custom task scheduler, I know that unhandled task exceptions will eventually escalate when the Task is cleaned up by the GC .

If you do not wait on a task that propagates an exception, or access its Exception property, the exception is escalated according to the .NET exception policy when the task is garbage-collected.

But who knows when that will happen in the environment in which the application executes - this can't be determined at design time.

To me, this means that unhandled exceptions in tasks can potentially go undetected - forever. That leaves a bad taste in my mouth.

If I want to ensure that unhandled task exceptions are escalated immediately, I can do the following in my task scheduler after the task is executed:

while ( taskOnQueue )
{
    /// dequeue a task

    TryExecuteTask(task);

    if (task.IsFaulted) 
    { 
        throw task.Exception.Flatten(); 
    }
}

But by doing this I basically guarantee that the exception will always terminate the process, regardless of how it may be handled by catching an AggregateException at Task.Wait() (or in a TaskScheduler.UnobservedException event handler).

Given these options - do I really have to choose one or the other?

Given these options - do I really have to choose one or the other?

Yes, you pretty much do. The only way to know for sure that a Task will not be Wait ed on is to find out that the Task is inaccessible from the executing program, which is what the GC does. Even if you could figure out whether some thread is currently Wait ing on the Task , that's not enough: the Task might be stored in some data structure somewhere and it will be Wait ed on at some point in the future.

The only alternative I can think of, and it's a really terrible option, especially from performance perspective, is to call GC.Collect() after every Task completes. That way, every Task that is unreachable at the point in time when it completes will be immediately considered unhandled. But even this isn't reliable, because a Task might become unreachable after it's completed.

To me, this means that unhandled exceptions in tasks can potentially go undetected - forever. That leaves a bad taste in my mouth.

This is true and you have no way of changing that. Point (1) is a lost cause.

I do not understand the point of the following:

if (task.IsFaulted) 
{ 
    throw task.Exception.Flatten(); 
}

This means that any task exception, even a handled one, will throw this exception This limits the tasks that you can reasonably create on this scheduler.

(Also I don't see why you flatten; even task.GetAwaiter().GetResult() is better. Probably, you should wrap instead of fake-rethrowing.)

Also see svicks answer.

Can't you just hook the unhandled exception event and report such cases to the developers? Unhandled task exceptions are sometimes bugs, but in my experience rarely fatal.

If you really don't need tasks to throw exceptions, you can schedule tasks using a helper method that wraps the task body with:

try {
 body();
} catch (...) { ... }

That would be a rather clean way to do it, flexible and does not depend on a custom scheduler.

Posting an answer so I can describe details which are important to the options as discussed in comments with usr .

There are (at least) two task-based approaches when using a separate method for logging or handling of task exceptions:

  1. A continuation task using task.ContinueWith()
  2. A second task which is created and scheduled immediately following the first

The distinction is important in my case because the custom scheduler is responsible for guaranteed FIFO scheduling of tasks which are presented to it across a guaranteed number of threads.

NB: due to the operation of the custom scheduler, the second task is guaranteed to run on the same thread as the first after the first task completes.

Option 1 - a continuation task

Task task = new Task(() => DoStuff());
task.ContinueWith(t => HandleTaskExceptions(t), customScheduler);
task.Start(customScheduler);

Option 2 - a second task scheduled immediately after the first

Task task = new Task(() => DoStuff());
task.Start(customScheduler);

Task task2 = new Task(() => HandleTaskExceptions(task));
task2.Start(customScheduler);

The distinction is in when the second task is scheduled to run.

Using option 1 with a task continuation means that :

The returned Task will not be scheduled for execution until the current task has completed, whether it completes due to running to completion successfully, faulting due to an unhandled exception, or exiting out early due to being canceled.

This means that the continuation may be placed at the end of a task queue, which further means that exceptions may not be handled or logged until after additional work has been done. This may impact upon the overall application state.

Using option 2, the exceptions are guaranteed to be handled immediately following completion of the first task, as the handler-task is the next in the queue.

HandleTaskExceptions() can be as simple as:

void HandleTaskExceptions(Task task)
{
    if (task.IsFaulted)
    {
        /// handle task exceptions
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM