Класс 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);
Приведенный выше код печатает мои отчеты о ходе работы в неправильном порядке. Я подозревал, что эта проблема связана с синхронизацией, так как когда я добавляю в//Some operation
место, все начинает работать правильно. Как мне добиться правильности без ненужногоThread.Sleep(10)
?
2 ответа
Вероятно, это зависит от вашего определения «правильности».
Согласно документации , обработчики прогресса вызываются с использованием текущего контекста синхронизации или (если такового нет) ThreadPool.
Теоретически вы можете настроить контекст синхронизации, который пытается иметь только один активный поток одновременно, но это может противоречить цели использования экземпляра Progress. Если вы хотите убедиться, что каждое значение обрабатывается по порядку, вы можете использовать что-то большее в духе Observable, напримерSubject
отSystem.Reactive
:
async Task Foo(ISubject<int> onProgressPercentChanged)
{
for (int i = 0; i < 1000; i++)
{
if (i % 10 == 0)
onProgressPercentChanged.OnNext(i / 10);
//Some operation
}
onProgressPercentChanged.OnCompleted();
}
var progress = new Subject<int>();
using (progress.Subscribe(i => Console.WriteLine(i + " %")))
{
await Foo(progress);
}
Или, если вам нужна многопоточная природа класса Progress, и вы согласны пропустить некоторые индикаторы прогресса, вы можете настроить свой обработчик, чтобы убедиться, что он не движется назад.
object mutex = new object();
int latestProgress = 0;
var progress = new Progress<int>(i =>
{
lock (mutex)
{
latestProgress = Math.Max(latestProgress, i);
if (latestProgress == i)
{
Console.WriteLine(i + " %");
}
}
});
await Foo(progress);
Я думаю, чтоProgress<T>
класс был разработан с учетом условий со специальнойустановлены, как приложения WinForms и WPF. В этих средах вызов будет синхронизирован, без перекрытия и с сохранением порядка FIFO. Консольные приложения не оснащены , поэтому вызываетсяThreadPool
, без какой-либо защиты от одновременного или внеочередного выполнения. Что вы можете сделать, так это либо установить соответствующее консольное приложение, либо использовать собственную реализациюIProgress<T>
интерфейс, который вызываетAction<T> handler
синхронно.
Для первого варианта требуется пакет AsyncEx NuGet:
AsyncContext.Run(async () =>
{
var progress = new Progress<int>(i => Console.WriteLine(i + " %"));
await Foo(progress);
});
The AsyncContext.Run
— это блокирующий вызов, аналогичныйметод. Он устанавливает специальныйSynchronizationContext
в текущей теме, очень похоже наApplication.Run
устанавливаетWindowsFormsSynchronizationContext
в потоке пользовательского интерфейса.
Для второго варианта см. этот ответ (SynchronousProgress<T>
сорт).