如何避免Blazor Server中CircuitHost组件的集合修改异常?
Blazor Server SSR模式下大量日志插入导致集合修改异常
我运行着Server Side Render模式的Blazor Web应用,用于监控数据库变更并将变更信息渲染至浏览器。
监控数据库变更并通知浏览器的代码
public async Task WatchAppLogChangesAsync(CancellationToken cancellationToken) { var pipeline = new EmptyPipelineDefinition<ChangeStreamDocument<AppLog>>().Match(x => x.OperationType == ChangeStreamOperationType.Insert); using (var cursor = await _appLogsCollection.WatchAsync(pipeline)) { await cursor.ForEachAsync(async change => { var playerId = change.FullDocument.UserId; if (_playerClientMap.TryGetValue(playerId, out var clients)) { foreach (var clientId in clients) { Console.WriteLine($"tell client: {clientId} when app log changed: {change.FullDocument.Message}"); await _hubContext.Clients.Client(clientId).SendAsync("AppLogUpdated", $"{change.FullDocument.Message}\r\n{change.FullDocument.RawData}"); } } }); } }
前端渲染日志的代码
protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; hubConnection = new HubConnectionBuilder() .WithUrl(Navigation.ToAbsoluteUri("/chathub")) .Build(); hubConnection.On<string>("AppLogUpdated", async (logMsg) => { // Console.WriteLine($"receive from hub2: {logMsg}"); messages.Add(logMsg); await InvokeAsync(StateHasChanged); }); await hubConnection.StartAsync(); await Send(); await GetLatestLogs(2); }
异常信息
warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100] Unhandled exception rendering component: Collection was modified; enumeration operation may not execute. System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.List`1.Enumerator.MoveNextRare() at System.Collections.Generic.List`1.Enumerator.MoveNext() at Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder) at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder) at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry) at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue() fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111] Unhandled exception in circuit 'ZqYGFNtGl3fN67EQprc7TtLm6bTk1DNfRUOpqxgDQJQ'. System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.List`1.Enumerator.MoveNextRare() at System.Collections.Generic.List`1.Enumerator.MoveNext() at Radzen.Blazor.Rendering.Markers`1.BuildRenderTree(RenderTreeBuilder __builder) at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder) at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment) at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry) at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue() Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
问题原因及解决方法
核心原因
异常本质是集合的并发修改:Blazor组件在UI线程渲染时会遍历messages集合(Radzen组件内部正在执行遍历操作),而SignalR的回调是在后台线程执行,此时调用messages.Add修改集合,List<T>并非线程安全集合,遍历过程中修改会触发该异常。
解决方法
方法1:加锁同步集合访问
通过锁确保集合的读写操作互斥,避免并发冲突:
- 在组件中定义锁对象和集合:
private readonly object _messagesLock = new object(); private List<string> messages = new List<string>();
- 修改SignalR回调中的集合添加逻辑:
hubConnection.On<string>("AppLogUpdated", async (logMsg) => { lock(_messagesLock) { messages.Add(logMsg); } await InvokeAsync(StateHasChanged); });
- 渲染时获取集合快照(避免渲染过程中集合被修改):
private List<string> GetMessagesSnapshot() { lock(_messagesLock) { return messages.ToList(); } }
- 组件绑定数据时使用快照:
<!-- 假设使用RadzenList组件展示日志 --> <RadzenList Data="@GetMessagesSnapshot()" TItem="string"> <Template Context="msg"> <p>@msg</p> </Template> </RadzenList>
方法2:使用线程安全集合并取快照渲染
用ConcurrentQueue<string>(线程安全的队列集合)替代List<T>,渲染时转换为List快照:
- 替换集合类型:
private ConcurrentQueue<string> messages = new ConcurrentQueue<string>();
- 修改SignalR回调的添加逻辑:
hubConnection.On<string>("AppLogUpdated", async (logMsg) => { messages.Enqueue(logMsg); await InvokeAsync(StateHasChanged); });
- 渲染时取快照:
<RadzenList Data="@messages.ToList()" TItem="string"> <Template Context="msg"> <p>@msg</p> </Template> </RadzenList>
额外优化
如果日志量极大,频繁触发StateHasChanged会导致UI频繁更新,可考虑批量更新:比如累计N条日志后再触发一次渲染,或设置固定间隔合并更新,减少渲染压力。
内容的提问来源于stack exchange,提问作者Penney Wang
相关产品推荐
相关产品推荐

