单例信息服务异步计算方案咨询:并发与重入问题排查
方案可行性与并发问题咨询
我实现了一个管理InformationBlock的单例组件SingletonInformationService,InformationBlock由Id和时间段标识,计算耗时数秒并存储于集合中。设计流程为:首个请求触发计算,后续请求等待计算完成后统一回调,核心代码如下:
public class SingletonInformationService { private readonly Collection<InformationBlock> blocks = new(); private object syncObject = new(); public void GetInformationBlock(Guid id, TimePersiod timePeriod, Action<InformationBlock> callOnFinish) { InformationBlock block = null; lock(syncObject) { // check out if the block already exists block = blocks.SingleOrDefault(b => b.Id ...); if (block == null) { block = new InformationBlock(...); blocks.Add(block); } } block?.BeginCalculation(callOnFinish); return true; } } public class InformationBlock { private Task calculationTask = null; private CalculationState isCalculating = CalculationState.Unknown; private List<Action<InformationBlock>> waitingRoom = new(); internal void BeginCalculation(Action<InformationBlock> callOnFinish) { if (isCalculating == CalculationState.Finished) { callOnFinish(this); return; } else if (isCalculating == CalculationState.IsRunning) { waitingRoom.Add(callOnFinish); return; } // add the first call to the waitingRoom waitingRoom.Add(callOnFinish); isCalculating = CalculationState.IsRunning; calculationTask = Task.Run(() => { // run the calculation}) .ContinueWith(taskResult => { //.. apply the calculation result to local properties this.Property1 = taskResult.Result.Property1; // set the state to mark this instance as complete isCalculating = CalculationState.Finished; // inform all calls about the result waitingRoom.ForEach(c => c(this)); waitingRoom.Clear(); }, TaskScheduler.FromCurrentSynchronizationContext()); } }
现咨询该方案是否可行,是否存在故障、死锁风险;BeginCalculation被并发调用时是否需要await calculationTask?
方案可行性与风险分析
可行性
整体设计思路是可行的:通过单例统一管理InformationBlock实例,避免重复计算,后续请求排队等待结果统一回调,能有效减少重复计算开销,符合业务需求。
潜在故障与死锁风险
- 线程安全隐患:
InformationBlock中的isCalculating状态切换、waitingRoom的增删操作未加锁,并发调用BeginCalculation时会出现竞态条件——比如多个线程同时进入Unknown状态分支,重复启动计算任务;List<Action<InformationBlock>>本身不是线程安全集合,并发添加会导致数据损坏。 - 死锁风险:
ContinueWith使用TaskScheduler.FromCurrentSynchronizationContext(),如果当前是UI同步上下文(如WPF/WinForms),计算任务完成后回调会被调度到UI线程,若回调逻辑中再次触发GetInformationBlock并进入lock,可能和UI线程调度冲突引发死锁;另外,若计算任务阻塞,同步上下文队列积压也会导致响应停滞。 - 异常未处理:如果
Task.Run中的计算抛出异常,taskResult.IsFaulted状态被忽略,isCalculating会一直停留在IsRunning,所有后续请求都会被加入waitingRoom但永远得不到回调,形成“请求黑洞”。 - 内存泄漏:
blocks集合只增不减,若Id和时间段组合过多,会导致内存持续上涨;若计算任务异常终止,waitingRoom中的委托会持有外部引用,无法被GC回收。
BeginCalculation是否需要await calculationTask?
不需要直接await,但要优化任务的异常处理和状态管理:
- 当前代码仅赋值
calculationTask,未跟踪任务异常,需在ContinueWith中判断taskResult.Status,处理Faulted和Canceled状态,避免isCalculating卡住。 - 若将
BeginCalculation改为异步方法(返回Task),可让调用方感知计算状态,但核心逻辑无需await——我们需要的是后台计算完成后统一通知所有等待回调,而非让单个调用方等待任务结束。
内容的提问来源于stack exchange,提问作者zimmybln
相关产品推荐
相关产品推荐

