Как синхронизировать общий IProgress <int>
У меня асинхронный метод DoStuffAsync
который порождает две задачи с Task.Run
, и обе задачи сообщают о своем прогрессе, используя один IProgress<int>
объект. С точки зрения пользователя есть только одна операция, поэтому отображаются два индикатора выполнения (по одному для каждогоTask
) не имело бы никакого смысла. Вот почемуIProgress<int>
общий. Проблема в том, что иногда пользовательский интерфейс получает уведомления о ходе выполнения в неправильном порядке. Вот мой код:
private async void Button1_Click(object sender, EventArgs e)
{
TextBox1.Clear();
var progress = new Progress<int>(x => TextBox1.AppendText($"Progress: {x}\r\n"));
await DoStuffAsync(progress);
}
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
var localPercentDone = Interlocked.Add(ref totalPercentDone, 10);
progress.Report(localPercentDone);
}
})).ToArray();
await Task.WhenAll(tasks);
}
В большинстве случаев уведомления расположены в правильном порядке, но иногда это не так:
Это вызывает ProgressBar
(не показан на скриншоте выше), чтобы неловко прыгать вперед и назад.
В качестве временного решения я добавил lock
внутри DoStuffAsync
метод, который включает вызов IProgress.Report
метод:
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
object locker = new object();
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
lock (locker)
{
totalPercentDone += 10;
progress.Report(totalPercentDone);
};
}
})).ToArray();
await Task.WhenAll(tasks);
}
Хотя это решает проблему, это вызывает у меня беспокойство, потому что я вызываю произвольный код, удерживая lock
. ВDoStuffAsync
фактически является частью библиотеки и может быть вызван с любым IProgress<int>
реализация в качестве аргумента. Это открывает возможность для сценариев взаимоблокировки. Есть ли лучший способ реализоватьDoStuffAsync
метод, без использования lock
, но с желаемым поведением в отношении порядка уведомлений?
4 ответа
Ваша проблема в том, что вам нужно приращение totalPercentDone
И призыв к Report
быть атомарным.
Нет ничего плохого в использовании lock
Вот. В конце концов, вам нужен способ сделать две операции атомарными. Если вы действительно не хотите использоватьlock
тогда вы могли бы использовать SemaphoireSlim:
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
var semaphore = new SemaphoreSlim(1,1);
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
await semaphore.WaitAsync();
try
{
totalPercentDone += 10;
progress.Report(totalPercentDone);
}
finally
{
semaphore.Release();
}
}
})).ToArray();
await Task.WhenAll(tasks);
}
Вы можете просто сообщить о дельтах и позволить обработке обрабатывать их:
private async void Button1_Click(object sender, EventArgs e)
{
TextBox1.Clear();
var totalPercentDone = 0;
var progress = new Progress<int>(x =>
{
totalPercentDone += x;
TextBox1.AppendText($"Progress: {totalPercentDone}\r\n"));
}
await DoStuffAsync(progress);
}
async Task DoStuffAsync(IProgress<int> progress)
{
await Task.WhenAll(Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
progress.Report(10);
}
})));
}
Вместо использования одного int для обеих задач вы можете использовать два отдельных int и взять наименьшее из них. По каждой Задаче нужно отчитываться до 100, а не до 50.
async Task DoStuffAsync(IProgress<int> progress)
{
int[] totalPercentDone = new int[2];
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
totalPercentDone[n - 1] += 10;
progress.Report(totalPercentDone.Min());
}
})).ToArray();
await Task.WhenAll(tasks);
}
Это расширение моих комментариев по вопросу
По сути, прогресс - это значение, ориентированное только на будущее. Что касается отчета о прогрессе, вероятно, вам никогда не придется сообщать о прогрессе, достигнутом в прошлом. Даже если вы это сделаете, в большинстве случаев сторона клиента / обработчика событий все равно отбрасывает полученные значения.
Проблема здесь / почему вам нужно синхронизировать отчеты в основном потому, что вы сообщаете о прогрессе типа значения, значение которого было скопировано, когда Report(T)
называется.
Вы можете просто избежать блокировки, сообщив об экземпляре ссылочного типа с последним достигнутым прогрессом:
public class DoStuffProgress
{
private volatile int _percentage;
public int Percentage => _percentage;
internal void IncrementBy(int increment)
{
Interlocked.Add(ref _percentage, increment);
}
}
Теперь ваш код выглядит так:
async Task DoStuffAsync(IProgress<DoStuffProgress> progress)
{
DoStuffProgress totalPercentDone = new DoStuffProgress();
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
totalPercentDone.IncrementBy(10);
// Report reference type object
progress.Report(totalPercentDone);
}
})).ToArray();
await Task.WhenAll(tasks);
}
Однако клиент может получить уведомление с повторяющимся значением:
Progress: 20
Progress: 20
Progress: 40
Progress: 40
Progress: 60
Progress: 60
Progress: 80
Progress: 80
Progress: 90
Progress: 100
Но значения никогда не должны выходить из строя.