2

I have an asynchronous method DoStuffAsync that spawns two tasks with Task.Run, and both tasks report their progress using a single IProgress<int> object. From the user's perspective there is only one operation, so showing two progress bars (one for each Task) wouldn't make any sense. This is why the IProgress<int> is shared. The problem is that sometimes the UI receives the progress notifications in incorrect order. Here is my code:

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);
}

Most of the time the notifications are in the correct order, but sometimes they are not:

Sreenshot

This causes the ProgressBar control (not shown in the above screenshot) to jump awkwardly back and forth.

As a temporary solution I have added a lock inside the DoStuffAsync method, that includes the invocation of the IProgress.Report method:

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);
}

Although this solves the problem, it causes me anxiety because I invoke arbitrary code while holding a lock. The DoStuffAsync method is actually part of a library, and could be called with a whatever IProgress<int> implementation as argument. This opens the possibility for deadlock scenarios. Is there a better way to implement the DoStuffAsync method, without using a lock, but with the desired behavior regarding the ordering of the notifications?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    There is multiple options actually. You have 2 task and 100% bar - you can either take just Max percent passed into Report (if they are same in nature, it will be simpliest approach), or split them 50% for one task, 50% for other, and each one of them contribute to it's separate instance, and you listen for combination of those two. – eocron Apr 17 '20 at 09:07
  • I don't see the lock as a trouble. Even external operations are involved, the locked object is declared locally and therefore no custom IProgress<> implementation can access it. A deadlock should be possible only in case of custom IProgress<> implementation hanging, that could cause the same trouble (task not working) anyway. – Rubidium 37 Apr 17 '20 at 09:23
  • If you declare your `totalPercentDone` as a Field and, in `DoStuffAsync`, you just: `await Task.WhenAll(Enumerable.Range(1, 2).Select(n => DoWorkAsync(progress)));` (resetting `totalPercentDone` in the button.Click). `DoWorkAsync()` is of course the worker method. There you can have: `progress.Report(Interlocked.Add(ref totalPercentDone, 10));`. It should also work if you press the Button repeatedly (but the results will accumulate. That Button needs to be disabled). – Jimi Apr 17 '20 at 09:47
  • 1
    The way I handled this in an UWP app (similar to a WinForm app) was simply ignoring smaller values in `ProgressChanged` . To me, there was no point to keep / display a progress made in the past. And I can have less code in a locked context. Maybe you have valid cases here, and you have got good answers. – weichch Apr 17 '20 at 10:15
  • @weichch this is definetely a solid option. I would like though to avoid moving the burden of order-preservation to the client side. Ideally the client code (the caller of the `DoStuffAsync` method) should have to just subscribe to the `Progress.ProgressChanged` event, and then everything should work as expected. – Theodor Zoulias Apr 17 '20 at 18:37
  • Sure - but in this case you need to report progress from a single context. As you might have noticed the accepted answer does not work in console apps, which means your implementation, if it replies on winform sync context, could be dangerous to callers from other types of applications. What about changing your progress value type to a reference type? The client shall receive the same progress object with latest progress value. And you could avoid unnecessary locking code. I will post an answer to extend this :) – weichch Apr 18 '20 at 06:58
  • @weichch regarding relying on a sync context, this is a given in my case. This question is specifically about how to display a well-behaved `ProgressBar` in a `System.Windows.Forms.Form`. Having a solution that also works well in Console applications would be nice, but not required. – Theodor Zoulias Apr 18 '20 at 07:57

4 Answers4

6

Your problem is that you need the increment of totalPercentDone AND the call to Report to be atomic.

There's nothing wrong with using a lock here. After all, you need some way to make the two operations atomic. If you really don't want to use lock then you could use a 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);
}
Sean
  • 60,939
  • 11
  • 97
  • 136
  • Thanks Sean for the answer. This one works well, and seems like a better solution than mine! Do you think that it's possible for a deadlock to occur, if it's used in combination with an `IProgress` implementation that has its own sync or async synchronization internally? – Theodor Zoulias Apr 17 '20 at 09:24
  • I can't see how using a `SemaphoreSlim` or a `lock` would cause a deadlock here, unless the `IProgress` implementation is buggy. – Sean Apr 17 '20 at 09:27
  • @TheodorZoulias - tried this answer, and mine and both fail this test - https://dotnetfiddle.net/kXPEm1 - am I doing something wrong? – Rand Random Apr 17 '20 at 09:35
  • Maybe you are right, and my fears are groundless. In any case your solution is clearly better than mine because it doesn't block any thread, so I'll accept it. :-) – Theodor Zoulias Apr 17 '20 at 09:37
  • @Rand Random you are right, the notifications are still messed up in Console applications. In WinForms they are OK though. Maybe it has to do with the synchronization context. – Theodor Zoulias Apr 17 '20 at 09:39
  • 3
    `Progress.IProgress.Report` doesn't wait until the handler is invoked; it only *queues* the progress report to the SyncCtx captured when the `Progress` was created. The WinForms SyncCtx handles them in order. Without a FIFO-queueing SyncCtx (i.e., in the general case), the reports could still be out of order. – Stephen Cleary Apr 17 '20 at 09:56
4

You could just report the deltas and let the handling handle them:

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);
        }
    })));
}
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • Has the Progress internal locking of somekind? Because if not your code is prone to lose updates and never reach 100% – Ackdari Apr 17 '20 at 13:42
  • If the `Progress` instance is created in the presence of a `SynchronizationContext`, link in an event handler of a Windows Forms control, it is guaranteed to be posted to that synchronization context. If not, you can use an ´ActionBlock`. – Paulo Morgado Apr 17 '20 at 14:21
  • 1
    Using a `Progress` like this is counter-intuitive, but I am upvoting it because it is a lock-free solution. :-) – Theodor Zoulias Apr 17 '20 at 18:57
3

Instead of using one int for both tasks you could use two individual ints, and take the smallest of them. Each Task need to report to 100, and not 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);
}
Rand Random
  • 7,300
  • 10
  • 40
  • 88
  • Thanks Random for the answer. I tested your solution. Unfortunately it produces notifications in the incorrect order sometimes, like my own first implementation ([screenshot](https://prnt.sc/s126yx)). – Theodor Zoulias Apr 17 '20 at 09:15
  • @TheodorZoulias - tested it, and `.Min()` seems to work at my end – Rand Random Apr 17 '20 at 09:24
  • I just tested the new `.Min()` version. There are still notifications out of order ([screenshot with .Min](https://prnt.sc/s12i2r)). – Theodor Zoulias Apr 17 '20 at 09:30
  • I would suggest using `totalPercentDone.Sum() / 2` to have a more smooth transition in cases where one task is taking way longer to do its job. – Ackdari Apr 17 '20 at 13:44
3

This is to extend my comments under question

Basically, progress is usually a forward-only value. With regards to reporting progress, it is likely that you never need to report a progress made in the past. Even you do, in most cases the client / event handler side would still drop such values received.

The problem here / why you need to synchronize reporting is mainly because you are reporting a progress of value type, whose value got copied when Report(T) is called.

You can simply avoid locking by reporting a reference type instance with the latest progress made:

public class DoStuffProgress
{
    private volatile int _percentage;

    public int Percentage => _percentage;

    internal void IncrementBy(int increment)
    {
        Interlocked.Add(ref _percentage, increment);
    }
}

Now your code looks like:

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);
}

The client, however, may receive notification with duplicate value:

Progress: 20
Progress: 20
Progress: 40
Progress: 40
Progress: 60
Progress: 60
Progress: 80
Progress: 80
Progress: 90
Progress: 100

But, the values should never be out of order.

weichch
  • 9,306
  • 1
  • 13
  • 25
  • Thanks weichch for the answer. This is a very interesting idea! Essentially you are reporting the same object over and over again. I think that you could make your idea even more appealing by using the `IProgress` object itself as the most-recent-value-holder (instead of the `DoStuffProgress` instance). You would just need to add one more member to the [`IProgress`](https://learn.microsoft.com/en-us/dotnet/api/system.iprogress-1) interface. A property `public T CurrentValue`. – Theodor Zoulias Apr 18 '20 at 07:21
  • Actually it's not that simple. Adding just the property `T CurrentValue { get; }` is not enough. The interface also needs the methods `void IncrementBy(T value);` and `void Report();`, so it becomes more complicated and less appealing. – Theodor Zoulias Apr 18 '20 at 08:41
  • The `IProgress` interface is for reporting progress. You could have your own `IProgressValue` interface which contains a value of T, and the implementation contains internal set. – weichch Apr 18 '20 at 09:25
  • I tried to implement just that, and found out that unless the interface has a method for incrementing atomically its internal value we are back in the initial problem of the out-of-order reporting. I am currently experimenting with this interface: `interface IProgressIncremental {T CurrentValue { get; } void Initialize(T initialValue, Func increment); void IncrementBy(T value);}`, which works but it is far from cute. – Theodor Zoulias Apr 18 '20 at 09:33