c# 在 Blazor Server 中处理并发事件和状态更新

来源:这里教程网 时间:2026-02-21 17:37:42 作者:

Blazor Server 的事件处理默认不是线程安全的

Blazor Server 使用 SignalR 长连接通信,所有 UI 事件(如

@onclick
)都在同一个逻辑“请求上下文”中同步执行,但不等于线程安全。多个用户操作或快速连续点击可能触发并发调用,而组件实例是按用户会话单例复用的,
StateHasChanged()
和字段更新若未加防护,会导致状态错乱或
ObjectDisposedException

常见错误现象:
System.ObjectDisposedException: Cannot access a disposed object
,尤其在异步操作完成前组件已卸载
典型场景:按钮连续点击触发同一
async Task
方法;后台任务(如
Timer
)更新组件字段后调用
StateHasChanged()
关键限制:Blazor Server 不允许从非渲染线程直接调用
StateHasChanged()
—— 必须通过
InvokeAsync()

InvokeAsync()
包裹所有跨上下文的状态更新

即使你在

async
方法中 await 了外部服务,回调一旦脱离原始渲染上下文(例如从
Task.Run
Timer
或第三方库回调中返回),就必须显式切回组件上下文才能安全更新状态或触发重绘。

private async Task HandleClick()
{
    // ❌ 危险:若 DoWorkAsync 内部用了 Task.Run 或延迟回调,后续更新可能并发
    var result = await DoWorkAsync();
    currentData = result;
    StateHasChanged(); // 可能抛 ObjectDisposedException
    // ✅ 正确:确保所有 UI 更新都经 InvokeAsync 调度
    await InvokeAsync(() =>
    {
        currentData = result;
        StateHasChanged();
    });
}
InvokeAsync(Action)
是线程安全的入口,Blazor 会排队执行并自动忽略已销毁组件的调用
不要对每个字段赋值都单独调用
InvokeAsync
;合并到一个委托里减少调度开销
如果方法本身被多次快速调用,仅靠
InvokeAsync
不足以防重复提交 —— 还需业务层节流或禁用按钮

避免在事件处理中裸写
Task.Run
或长时间运行同步代码

Blazor Server 渲染线程(即

SynchronizationContext
)是单线程的。在事件处理器里直接调用阻塞式 IO 或 CPU 密集型代码(如
Thread.Sleep
、大数组排序、XML 解析),会卡住整个用户的会话通道,影响响应甚至引发超时断连。

错误示例:
Task.Run(() => { Thread.Sleep(2000); return GetData(); })
—— 没解决根本问题,反而增加线程调度负担
正确做法:将真正耗时操作移至后台服务(如
IHostedService
),用
Channel<t></t>
EventCallback
通知组件;或使用真正异步 API(
HttpClient.GetAsync
Stream.ReadAsync
若必须同步计算,至少加取消检查:
if (CancellationToken.IsCancellationRequested) return;
,并在组件
Dispose
时传递取消令牌

CancellationToken
主动取消挂起的异步操作

用户导航离开页面、组件被销毁后,未完成的

await
任务仍可能继续执行,并在完成后尝试调用
InvokeAsync
—— 此时组件已释放,
InvokeAsync
会静默丢弃该调用,但后台任务仍在浪费资源。

private CancellationTokenSource? _cts;
protected override void OnInitialized()
{
    _cts = new();
}
private async Task LoadData()
{
    try
    {
        var data = await _api.GetDataAsync(_cts.Token); // 传入 token
        await InvokeAsync(() =>
        {
            items = data;
            StateHasChanged();
        });
    }
    catch (OperationCanceledException)
    {
        // 被主动取消,无需处理
    }
}
public void Dispose()
{
    _cts?.Cancel();
    _cts?.Dispose();
}
每次启动新异步操作前应取消旧的
CancellationTokenSource
,防止竞态残留
不要依赖
IsDisposed
判断组件状态 ——
InvokeAsync
已内置防护,重点是及时取消上游 I/O
第三方库若不支持
CancellationToken
,考虑用
Task.WaitAsync(TimeSpan, CancellationToken)
包装超时
Blazor Server 的并发风险不在“多线程争抢”,而在“生命周期与异步生命周期错位”。最易被忽略的是:**你以为的“顺序执行”其实依赖于 SignalR 消息到达顺序和服务器端队列调度,而非代码书写顺序**。任何绕过
InvokeAsync
的 UI 更新、任何未绑定取消令牌的 await、任何未清理的后台计时器,都会在高交互场景下暴露问题。

相关推荐