Progress<T>类同步问题:进度报告顺序错乱如何解决?
问题分析与解决
问题代码
Task Foo(IProgress<int> onProgressPercentChanged){ return Task.Run(() =>{ for (int i = 0; i < 1000; i++){ if (i % 10 == 0) onProgressPercentChanged.Report(i / 10); //Some operation } }); } var progress = new Progress<int>(i => Console.WriteLine(i + " %")); await Foo(progress); Thread.Sleep(10000);
进度报告打印顺序混乱的核心原因是:Progress<T>的Report方法会将回调异步调度到创建Progress<T>时捕获的同步上下文(控制台程序无UI同步上下文,会 fallback 到线程池线程)。当循环执行速度远快于回调打印速度时,多个Report请求会被线程池的不同线程处理,线程调度的随机性直接导致打印顺序错乱。加Thread.Sleep(10)只是靠延迟“碰巧”让回调赶上下一次Report前执行,本质是用延迟掩盖线程竞争问题。
解决方法
方法1:给回调加锁,确保打印操作线程安全
直接在Progress<T>的回调逻辑中加锁,保证所有打印操作按顺序执行:
var lockObj = new object(); var progress = new Progress<int>(i => { lock(lockObj) { Console.WriteLine(i + " %"); } }); await Foo(progress);
这个方法改动最小,适合只需要保证输出顺序的场景。
方法2:实现同步版的IProgress<T>
如果不需要跨线程调度回调(比如控制台程序无需切换到UI线程),可以自己实现一个同步执行回调的IProgress<T>,从根源避免异步调度带来的顺序问题:
public class SyncProgress<T> : IProgress<T> { private readonly Action<T> _handler; public SyncProgress(Action<T> handler) { _handler = handler ?? throw new ArgumentNullException(nameof(handler)); } public void Report(T value) { _handler(value); } }
使用时替换Progress<T>即可:
var progress = new SyncProgress<int>(i => Console.WriteLine(i + " %")); await Foo(progress);
这种方式下,回调会在调用Report的线程(也就是Task.Run的工作线程)上同步执行,天然保证顺序。
方法3:控制Report的调度节奏(可选优化)
如果循环内的操作确实极快,还可以在Foo方法内控制Report的调用节奏,确保上一次回调完成后再发送下一次进度:
Task Foo(IProgress<int> onProgressPercentChanged) { return Task.Run(async () => { var lastCallbackTask = Task.CompletedTask; for (int i = 0; i < 1000; i++) { if (i % 10 == 0) { var currentProgress = i / 10; // 等待上一次回调执行完成 await lastCallbackTask; // 调度下一次回调并保存任务引用 lastCallbackTask = Task.Run(() => onProgressPercentChanged.Report(currentProgress)); } //Some operation } // 等待最后一次回调完成 await lastCallbackTask; }); }
内容的提问来源于stack exchange,提问作者Johnny Bravo
相关产品推荐
相关产品推荐

