先上堆栈

TaskCanceledException at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at 
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at 
System.Windows.Threading.DispatcherOperation.Wait(TimeSpan timeout) at
System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherOperation operation, CancellationToken cancellationToken, TimeSpan timeout) at 
System.Windows.Threading.Dispatcher.Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken, TimeSpan timeout) at
MS.Internal.WeakEventTable.OnShutDown() at 
MS.Internal.ShutDownListener.HandleShutDown(Object sender, EventArgs e) 

##背景

问题是这样的,从5月份开始,陆续有公司发现自己的WPF软件收到大量用户报告TaskCanceledException 异常,

就是上面堆栈中的信息,有的公司一个月内达到了惊人的150k的异常数据。我们的软件也在当月报了15k的异常。

诱因

原因来自于微软的18年6月预览版质量汇总补丁(KB 4229726),所以就是微软更新更炸了。

官方描述如下:

In certain .NET applications, timing issues in the finalizer thread could potentially cause exceptions during AppDomain or process shutdown.  This is generally seen in applications that do not correctly shut down Dispatchers running on worker threads prior to process or AppDomain shutdown.  Such applications should take care to properly manage the lifetime of Dispatchers. 

翻译过来就是

对于某些特定的.NET应用程序(注:目前仅影响WPF),在AppDomain或者进程关闭时,Finalizer线程的计时问题可能会引发异常。这个问题通常出现在关闭期间,这些应用程序未能够正常关闭工作线程的Dispatcher。因此这些应用需要合理的管理Dispatcher的生命周期。

根因

时机问题来源:WeakEventTableOnShutDown()方法中,指定了300ms的超时(注:部分开发评论这个时间是arbitrary 武断的)

代码如下:

private void OnShutDown()
{
    if (CheckAccess())
    {
        Purge(true);
 
        // remove the table from thread storage
        _currentTable = null;
    }
    else
    {
        // if we're on the wrong thread, try asking the right thread
        // to do the job.  (DomainUnload arrives on finalizer thread - DDVSO 543980)
        bool succeeded = false;
        try
        {
            Dispatcher.Invoke((Action)OnShutDown, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(300));
            succeeded = true;
        }
        catch (TimeoutException)
        {
        }
 
        // if that didn't work (because Dispatcher was busy or not pumping),
        // do the work on the wrong thread, but don't touch thread-statics.
        // This won't do everything, but it will do enough to support
        // some useful scenarios (such as DevDiv Bugs 121070).
        if (!succeeded)
        {
            Purge(true);
        }
    }
}

可以看到,在错误线程调用该方法时,进入else,然后触发超时。源代码中针对TimeoutException进行了catch,但是没有处理TaskCanceledException 。而从堆栈信息上看,很可能这次更新将内部实现改为了异步任务。

影响范围

按官方文档解释,目前仅影响4.7.2上运行的部分WPF程序

解决方案

直接方案

这个补丁上线时,提供了一个开关。只要在app.config里面添加

<configuration>
    <runtime>
        <AppContextSwitchOverrides value="Switch.MS.Internal.DoNotInvokeInWeakEventTableShutdownListener=true"/>
    </runtime>
</configuration>

该方案能够有助于缓解(alleviate)该问题, 而并不能消除(eliminate)

根本方案

1、清理代码中跨线程调用OnShutDown()方法

2、减少关闭期间Dispatcher的调用

参考链接:


本文会经常更新,请阅读原文: https://xinyuehtx.github.io/post/WPF%E7%A8%8B%E5%BA%8F%E5%9C%A8shutdown%E6%9C%9F%E9%97%B4%E5%BC%95%E5%8F%91%E7%9A%84TaskCanceledException.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名黄腾霄(包含链接: https://xinyuehtx.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系