Blazor Server服务端组件非生命周期调用异步方法死锁问题求解
Blazor Server 为每个用户会话维护一个专属的ComponentRenderingSynchronizationContext同步上下文,所有和组件渲染、状态变更相关的任务都会进入这个上下文的队列,队列默认是串行执行的,同一时间只会有一个任务在运行,不管底层调度用了多少线程,都要等前一个队列任务执行完成才能跑下一个。
各测试用例的运行逻辑解释
可运行:
@TaskService.RequestGoogle()
这个方法是纯同步实现,内部直接调用HttpClient.GetAsync().Result阻塞等待结果。HttpClient的异步IO操作完成后的默认延续任务运行在ThreadPool线程,不会捕获Blazor的同步上下文,所以不需要等待Blazor上下文队列调度,阻塞可以正常拿到结果,不会死锁。死锁:
@TaskService.RequestGoogleAsync().ResultRequestGoogleAsync是async方法,内部await _httpClient.GetAsync时默认会捕获当前的Blazor同步上下文,await之后的代码(读取StatusCode的逻辑)需要排队到Blazor的同步上下文队列才能执行。但你在渲染线程调用.Result已经把当前正在执行的上下文任务阻塞了,队列无法调度后续的延续任务运行,就形成了「等待任务完成→任务需要排队到当前阻塞的上下文才能完成」的死锁。死锁:
@TaskService.RequestGoogleAsync().ConfigureAwait(false).GetAwaiter().GetResult()
这里的ConfigureAwait(false)是加在调用方拿到的返回Task上的,只能影响调用方等待完成后的后续逻辑,完全管不到RequestGoogleAsync方法内部的await逻辑。内部的await还是捕获了Blazor同步上下文,延续任务依然要进上下文队列等待调度,和上一个场景一样触发死锁。如果要让ConfigureAwait(false)生效,需要在RequestGoogleAsync内部的每个await都加上配置,比如await _httpClient.GetAsync(...).ConfigureAwait(false),才能避免内部捕获Blazor上下文。可运行:
@(Task.Run(() => TaskService.RequestGoogleAsync()).Result)Task.Run会把RequestGoogleAsync的执行调度到ThreadPool线程,这个线程不存在Blazor的同步上下文,所以方法内部await时捕获的上下文为null,IO完成后的延续任务直接在ThreadPool运行,不需要进Blazor的同步上下文队列,所以渲染线程的阻塞等待可以正常拿到结果,不会死锁。编译失败:
@await TaskService.RequestGoogleAsync()
Blazor的Razor渲染逻辑是同步执行的,渲染委托本身不支持异步写法,异步获取数据的逻辑必须放到组件自带的异步生命周期钩子(如OnInitializedAsync、OnParametersSetAsync)中执行,把结果存到组件的字段/属性后,再在渲染模板中引用这个存储的结果。
最佳实践
不要在组件渲染逻辑中同步阻塞异步方法,严格遵循Blazor的开发规范:所有异步IO操作都放在异步生命周期钩子中执行,结果存入组件状态后再渲染。
内容的提问来源于stack exchange,提问作者Tomas Chabada

