I have a C# Extension method that can be used with tasks to make sure any exceptions thrown are at the very minimum observed, so as to not crash the hosting process. In .NE
I see a few problems with this approach.
First, there's a definite race condition. When TaskEx.Run
returns a task, it has merely queued a request to the thread pool; the task has not necessarily yet completed.
Second, you're running into a some garbage collector details. When compiled in debug - and really, whenever else the compiler feels like it - the lifetimes of local variables (i.e., res
) are extended to the end of the method.
With these two problems in mind, I was able to get the following code to pass:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
However, there are still two problems:
There is still (technically) a race condition. Although UnobservedTaskException
is raised as a result of a task finalizer, there is no guarantee AFAIK that it is raised from the task finalizer. Currently, it appears to be, but that seems to me to be a very unstable solution (considering how restricted finalizers are supposed to be). So, in a future version of the framework, I wouldn't be too surprised to learn that the finalizer merely queues an UnobservedTaskException
to the thread pool instead of executing it directly. And in that case you can no longer depend on the fact that the event has been handled by the time the task is finalized (an implicit assumption made by the code above).
There is also a problem regarding modification of global state (UnobservedTaskException
) within a unit test.
Taking both of these problems into account, I end up with:
var mre = new ManualResetEvent(initialState: false);
EventHandler subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
if (!mre.WaitOne(10000))
Assert.Fail();
}
finally
{
TaskScheduler.UnobservedTaskException -= subscription;
}
Which also passes but is of rather questionable value considering its complexity.