Blazor中使用Task.WhenAll()执行同步任务时出现阻塞问题求助
Blazor任务同步阻塞问题的排查与修复
问题核心
你遇到的Task.WhenAll()阻塞问题,本质是进度检查任务的退出逻辑存在竞态,加上后台线程调用UI更新方法引发的上下文冲突,导致两个任务看似完成但主线程无法继续执行。
可能的原因
- 进度检查循环的竞态问题:同步任务完成后,进度检查任务可能正处于
Task.Delay(3000)的等待中,即便ProductSuccessCount已经等于ProductCount,也无法及时退出循环。 - 后台线程调用UI更新的冲突:在
Task.Run的后台线程中直接调用StateHasChanged(),Blazor的UI更新需要在同步上下文执行,后台线程调用会引发上下文阻塞,干扰任务完成信号的传递。 - 未处理的隐式异常:即便日志显示任务执行到最后一行,也可能存在未捕获的API请求异常,导致进度检查任务进入Faulted状态,进而让
Task.WhenAll()看似阻塞。
修复方案
方案一:添加取消令牌,主动终止进度检查任务
通过取消令牌,在同步任务完成后主动终止进度检查,避免不必要的等待:
public async Task SyncBtnClick() { string SyncId = Guid.NewGuid().ToString(); ProductSuccessCount = 0; GetItemListDto = await ProductService.GetItemList(MallId); ProductCount = GetItemListDto.Response.TotalCount; SyncState = false; await ModalSync.Show(); // 创建取消令牌源 using var cts = new CancellationTokenSource(); Task syncMallProductTask = Task.Run(async () => { try { await ProductService.SyncMallProduct(SyncId, MallId); } finally { // 同步任务完成后,主动取消进度检查 cts.Cancel(); } }); Task progressCheckTask = Task.Run(async () => { try { while (!cts.Token.IsCancellationRequested) { if (ProductSuccessCount >= ProductCount) break; // 延迟时也监听取消信号 await Task.Delay(3000, cts.Token); ProductSuccessCount = await _http.GetFromJsonAsync<int>($"api/Product/GetProgress?SyncGuid={SyncId}", cts.Token); // 确保在UI线程执行状态更新 await InvokeAsync(StateHasChanged); } } catch (OperationCanceledException) { // 忽略取消异常,任务正常结束 } }, cts.Token); await Task.WhenAll(syncMallProductTask, progressCheckTask); // 后续异常处理与业务逻辑不变 if (syncMallProductTask.IsFaulted) { Log.Warning("syncMallProductTask任務出現異常: " + syncMallProductTask.Exception); } else if (progressCheckTask.IsFaulted) { Log.Warning("progressCheckTask任務出現異常: " + progressCheckTask.Exception); } else { Log.Information("子任務已完成"); SyncState=true; await Task.Delay(3000); await MallService.MallsSyncInitList(); Mall = MallService.Malls.ToList(); StateHasChanged(); } }
方案二:移除不必要的Task.Run,利用Blazor异步上下文
Blazor组件的异步方法本身运行在UI上下文,无需额外用Task.Run包装异步IO任务,减少上下文切换开销:
public async Task SyncBtnClick() { string SyncId = Guid.NewGuid().ToString(); ProductSuccessCount = 0; GetItemListDto = await ProductService.GetItemList(MallId); ProductCount = GetItemListDto.Response.TotalCount; SyncState = false; await ModalSync.Show(); // 直接调用异步方法,无需Task.Run Task syncMallProductTask = ProductService.SyncMallProduct(SyncId, MallId); Task progressCheckTask = CheckProgressAsync(SyncId); await Task.WhenAll(syncMallProductTask, progressCheckTask); // 后续处理逻辑不变... } // 单独提取进度检查方法,运行在UI上下文 private async Task CheckProgressAsync(string syncId) { while (ProductSuccessCount < ProductCount) { await Task.Delay(3000); ProductSuccessCount = await _http.GetFromJsonAsync<int>($"api/Product/GetProgress?SyncGuid={syncId}"); // 此时在UI上下文,直接调用StateHasChanged即可 StateHasChanged(); } }
关键注意事项
- UI更新必须在UI线程执行:如果在后台线程中需要更新UI,必须使用
InvokeAsync(StateHasChanged),避免上下文冲突。 - 取消令牌的使用:添加取消机制能确保任务之间的协作,避免无意义的等待。
- 避免滥用
Task.Run:Blazor中仅在处理CPU密集型操作时才需要用Task.Run,异步IO任务直接调用即可。
内容的提问来源于stack exchange,提问作者Edemilorhea
相关产品推荐
相关产品推荐

