Server Side Blazor消息系统中CancellationTokenSource已释放问题排查
问题描述
我负责的项目基于Server Side Blazor搭建内置消息系统,使用SQL Server与Firebase:消息收发通过SQL Server实现,实时消息推送依赖Firebase。
当用户选择对话线程后,客户端会订阅Firebase中的消息及对话方状态。由于这些操作耗时较长,我采用先展示界面、后台执行操作的方案。但问题在于,用户可能在完成前一线程的所有订阅操作前就切换到其他线程。
为处理该场景,我尝试编写如下代码:
@code { private bool _processing = true; private CancellationTokenSource _cts = new CancellationTokenSource(); private CancellationToken? _token; private Task? _slowOperations; private async Task GetMessagesAsync() { _processing = true; // SQL Server operations _processing = false; StateHasChanged(); if (_slowOperations != null && !_slowOperations.IsCompletedSuccessfully) { _cts.Cancel(); _cts.Dispose(); _cts = new CancellationTokenSource(); _token = _cts.Token; } _slowOperations = Task.Run(() => ProceedSlowOperations(_token!.Value)); } private async Task ProceedSlowOperations (CancellationToken cancellationToken) { await Task.Delay(5000); if (!cancellationToken.IsCancellationRequested) { // make firebase subscriptions. } }
预期效果是用户多次切换线程时,仅在最后一次切换5秒后执行订阅,重置之前的调用。但实际使用中,切换线程的某些时段会出现The CancellationTokenSource has been disposed.错误,即使移除所有_cts.Dispose()调用,错误仍会发生。请问我哪里处理不当?
问题分析与修复方案
错误原因
- 令牌绑定已释放的Cts:传递给旧
ProceedSlowOperations任务的_token,指向的是已被Dispose()的CancellationTokenSource。当旧任务后续检查cancellationToken.IsCancellationRequested时,会触发已释放资源的错误。 - 状态更新时序混乱:更新
_token为新Cts的令牌后,旧任务仍持有旧令牌(对应已释放的Cts),导致资源访问冲突。 - 并发操作未同步:快速切换线程时,多个
GetMessagesAsync调用可能同时操作_cts和_slowOperations,引发状态不一致。
修复后的代码
@code { private bool _processing = true; private CancellationTokenSource? _currentCts; private Task? _slowOperations; private async Task GetMessagesAsync() { _processing = true; // SQL Server operations _processing = false; StateHasChanged(); // 取消并清理旧任务 if (_slowOperations != null && !_slowOperations.IsCompleted) { _currentCts?.Cancel(); try { // 等待旧任务完成(包括取消),避免资源泄漏 await _slowOperations; } catch (OperationCanceledException) { // 取消异常无需处理 } finally { _currentCts?.Dispose(); _currentCts = null; _slowOperations = null; } } // 创建新的Cts和任务 _currentCts = new CancellationTokenSource(); var token = _currentCts.Token; _slowOperations = ProceedSlowOperations(token); } private async Task ProceedSlowOperations (CancellationToken cancellationToken) { await Task.Delay(5000, cancellationToken); // 将令牌传入Delay,取消时立即终止 if (!cancellationToken.IsCancellationRequested) { // 执行Firebase订阅操作 } } }
关键改进点
- 为每个新任务创建独立的
CancellationTokenSource,避免旧令牌与新Cts绑定混乱。 - 取消旧任务后等待其完成再清理资源,确保旧任务不会访问已释放的Cts。
- 将
CancellationToken传递给Task.Delay,取消时Delay会立即终止,无需等待5秒。 - 移除全局
_token变量,直接使用当前Cts的令牌,避免引用过时资源。
内容的提问来源于stack exchange,提问作者Michel E
相关产品推荐
相关产品推荐

