Progress报告与Task续接的竞态条件分析及异步实现疑问
问题与解答
背景概述
我有一个LogWriter类,接收文件路径和System.Windows.Forms.TextBox,通过public void Write(string progress)方法同时写入文件和文本框:
- 内部使用开启
AutoFlush的StreamWriter追加写入文件,因此实现了IDisposable接口以释放流 - 每次调用
Write仅设置文本框的Text属性
在另一个类的RunProcess方法中,会启动外部进程,并通过ErrorDataReceived和OutputDataReceived事件的处理器,调用IProgress<string>实例的Report()方法输出日志。
异步按钮点击实现
using(LogWriter logger = new LogWriter(filePath, txtProgress)) { IProgress<string> progress = new Progress<string>(s => logger.Write(s)); await Task.Run(() => this._C.RunProcess(progress)); } //释放点
同步处理器的ContinueWith实现
LogWriter logger = new LogWriter(filePath, txtProgress); Task t = Task.Run(() => this._C.RunProcess(progress)); t.ContinueWith((_) => { coreCurrentLogger.Dispose(); }, TaskScheduler.FromCurrentSynchronizationContext());
已知Progress.Report()会通过SynchronizationContext.Post()将写入委托排入UI线程队列延后执行,当前上下文为UI线程上下文。两种实现运行结果均符合预期,现针对以下问题解答:
1. 是否存在竞态条件导致LogWriter提前释放?
两种实现都不会出现LogWriter在所有Report委托执行完成前被释放的竞态条件,核心原因:
- 异步
await版本:Task.Run执行的RunProcess任务,会在进程所有输出事件处理完成(即所有Report调用完毕)后才结束。await会等待该任务完成,之后才会进入using块的释放逻辑。而UI队列中的写入委托,会在await恢复后(UI线程空闲时)执行,此时LogWriter还未被释放。 ContinueWith版本:ContinueWith指定了UI线程调度器,会在RunProcess任务完成后,才在UI线程执行Dispose。RunProcess完成意味着所有Report已调用,而UI线程会先执行队列中已排入的写入委托,再执行Dispose逻辑,不会提前释放。
2. 两种实现的核心差异
- 资源管理:异步版本用
using块自动处理Dispose,符合C#资源管理规范,代码简洁不易出错;ContinueWith版本需手动调用Dispose,且代码中存在变量名笔误(coreCurrentLogger应为logger),容易引发bug。 - 上下文处理:异步
await会自动恢复到原UI上下文,无需额外配置;ContinueWith必须显式传入TaskScheduler.FromCurrentSynchronizationContext(),否则可能在后台线程执行Dispose,导致操作UI控件出错。 - 异常处理:异步版本可直接用
try/catch包裹await逻辑,统一处理所有异常;ContinueWith需手动检查任务的IsFaulted状态,异常处理流程更繁琐。 - 代码可读性:异步
await采用线性写法,执行顺序直观;ContinueWith是回调式结构,逻辑拆分,可读性相对较差。
3. 第一个示例是否需要添加ConfigureAwait(true)?
不需要。ConfigureAwait(true)是await的默认行为,无需显式指定。即使添加,也只是冗余代码,不会改变执行逻辑。
内容的提问来源于stack exchange,提问作者Dan Def
相关产品推荐
相关产品推荐

