Test for UnObserved Exceptions

后端 未结 2 860
长发绾君心
长发绾君心 2021-01-12 16:04

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

2条回答
  •  夕颜
    夕颜 (楼主)
    2021-01-12 16:32

    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.

提交回复
热议问题