Как синхронизировать общий IProgress <int> - PullRequest
3 голосов
/ 17 апреля 2020

У меня есть асинхронный метод 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);
}

В большинстве случаев уведомления находятся в правильном порядке, но иногда это не так:

Sreenshot

Это приводит к тому, что элемент управления 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> в качестве аргумента. Это открывает возможность тупикового сценария ios. Есть ли лучший способ реализовать метод DoStuffAsync, без использования lock, но с требуемым поведением относительно порядка уведомлений?

Ответы [ 4 ]

4 голосов
/ 17 апреля 2020

Ваша проблема в том, что вам нужно увеличение totalPercentDone И вызов Report для атома c.

Нет ничего плохого в использовании lock здесь. В конце концов, вам нужно каким-то образом сделать две операции атома c. Если вы действительно не хотите использовать 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);
}
3 голосов
/ 18 апреля 2020

Это расширение моих комментариев под вопросом

По существу, progress обычно является значением только для форварда. Что касается сообщения о прогрессе , вполне вероятно, что вам никогда не нужно сообщать о прогрессе, достигнутом в прошлом. Даже если вы это сделаете, в большинстве случаев клиент / обработчик событий будет по-прежнему отбрасывать полученные значения.

Проблема в том, почему вам нужно синхронизировать отчеты, главным образом потому, что вы сообщаете о прогрессе типа значения, значение которого копируется при вызове 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

Но значения никогда не должны быть не в порядке.

3 голосов
/ 17 апреля 2020

Вы можете просто сообщить о дельтах и ​​позволить обработчикам справиться с ними:

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);
        }
    })));
}
3 голосов
/ 17 апреля 2020

Вместо использования одного целого числа для обеих задач вы можете использовать два отдельных целых числа и взять наименьшее из них. Каждое задание нужно сообщать до 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);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...