You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

关闭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

火山引擎 最新活动