关闭WPF应用触发TaskCanceledException,寻求高效解决办法
解决后台线程Dispatcher.Invoke在程序关闭时抛出TaskCanceledException的问题
这个问题的核心原因是:当你通过窗口关闭按钮或任务栏关闭程序时,WPF的Dispatcher消息循环已经开始终止,但你的后台线程还在尝试调用Dispatcher.Invoke——此时Invoke的任务会被取消,从而抛出TaskCanceledException。虽然你把线程设为Background=true,但这只意味着程序退出时线程会被强制终止,却无法阻止它在终止前执行的Invoke操作触发异常。
下面是几种比单纯捕获异常更优雅的解决方案:
1. 使用CancellationToken优雅终止线程
这是最规范的方式,通过令牌让线程主动感知程序关闭信号,提前退出循环,避免执行无效的Invoke操作。
步骤:
- 定义类级别的
CancellationTokenSource用于管理取消信号 - 在窗口关闭时触发取消
- 修改线程方法,检查取消令牌并响应
// 类级别字段 private CancellationTokenSource _progressCts; private void SongChange(object sender, RoutedEventArgs e) { SongChangeAction(); songLength = (int)BGMPlayer.NaturalDuration.TimeSpan.TotalSeconds; // 先取消之前的进度线程(防止重复启动) _progressCts?.Cancel(); _progressCts?.Dispose(); _progressCts = new CancellationTokenSource(); songProgress = new Thread(() => SongProgressUpdate(_progressCts.Token)) { IsBackground = true }; songProgress.Start(); } private void SongProgressUpdate(CancellationToken cancelToken) { while (!cancelToken.IsCancellationRequested) { try { // 将取消令牌传入Invoke,当信号触发时会提前终止Invoke Dispatcher.Invoke(() => { workingResources.SongProgress = BGMPlayer.Position.TotalSeconds / songLength * 100; }, cancelToken); } catch (TaskCanceledException) { // 捕获取消异常后直接退出循环 break; } // 用WaitHandle替代Thread.Sleep,让线程能及时响应取消信号 cancelToken.WaitHandle.WaitOne(1000); } } // 窗口Closing事件处理(记得在XAML中绑定该事件) private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { _progressCts?.Cancel(); _progressCts?.Dispose(); }
2. 检查Dispatcher的关闭状态
在执行Dispatcher.Invoke前,先判断Dispatcher是否已经开始关闭,避免执行无效操作:
private void SongProgressUpdate() { while (true) { // 检查Dispatcher是否已开始关闭,是则退出循环 if (Dispatcher.HasShutdownStarted) break; Dispatcher.Invoke(() => { // 双重检查,防止在等待Invoke期间Dispatcher关闭 if (!Dispatcher.HasShutdownStarted) { workingResources.SongProgress = BGMPlayer.Position.TotalSeconds / songLength * 100; } }); Thread.Sleep(1000); } }
这种方式不需要额外的取消令牌,但Thread.Sleep会让线程无法及时响应关闭信号(最多延迟1秒),所以优先级低于第一种方案。
3. 使用DispatcherTimer替代后台线程
其实你完全可以用WPF自带的DispatcherTimer来实现进度更新,它本身就在Dispatcher线程上执行,不需要手动处理跨线程调用,也不会出现关闭时的异常问题:
private DispatcherTimer _progressTimer; private void SongChange(object sender, RoutedEventArgs e) { SongChangeAction(); songLength = (int)BGMPlayer.NaturalDuration.TimeSpan.TotalSeconds; // 停止之前的定时器 _progressTimer?.Stop(); _progressTimer = new DispatcherTimer(); _progressTimer.Interval = TimeSpan.FromSeconds(1); _progressTimer.Tick += (s, args) => { workingResources.SongProgress = BGMPlayer.Position.TotalSeconds / songLength * 100; }; _progressTimer.Start(); } // 窗口Closing事件中停止定时器 private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { _progressTimer?.Stop(); }
这是最简洁的方案,因为DispatcherTimer会自动跟随Dispatcher的生命周期,程序关闭时定时器会自动停止,无需额外处理线程同步问题。
内容的提问来源于stack exchange,提问作者Null Reference Exception




