4
private static async Task FuncAsync(DataTable dt, DataRow dr)
{
    try
    {
        await Task.Delay(3000); //assume this is an async http post request that takes 3 seconds to respond
        Thread.Sleep(1000) //assume this is some synchronous code that takes 2 second
    }
    catch (Exception e)
    {
        Thread.Sleep(1000); //assume this is synchronous code that takes 1 second
    }
}
private async void Button1_Click(object sender, EventArgs e)
{
    List<Task> lstTasks = new List<Task>();

    DataTable dt = (DataTable)gridview1.DataSource;

    foreach (DataRow dr in dt.Rows)
    {
        lstTasks.Add(FuncAsync(dr["colname"].ToString());                
    }            

    while (lstTasks.Any())
    {   
        Task finishedTask = await Task.WhenAny(lstTasks);
        lstTasks.Remove(finishedTask);
        await finishedTask;
        progressbar1.ReportProgress();
    }                        
}

Assuming the datatable has got 10000 rows.

In the code, on button click, at the 1st iteration of the for loop, an async api request is made. While it takes 3 seconds, the control immediately goes to the caller. So the for loop can make the next iteration, and so on.

When the api response arrives, the code below the await runs as a callback. Thus blocking the UI thread and any incomplete for loop iterations will be delayed until the callback completes irrespective of whether I use await WhenAny or WhenAll.

All code runs on the UI thread due to the presence of synchronization context. I can do ConfigureAwait false on Task.Delay so the callbacks run on separate threads in order to unblock the ui thread.

Say 1000 iterations are made when the 1st await returns and when the 1st iterations await call back runs the following iterations will have completed completed awaits so their callbacks will run. Effectively callbacks will run one after the other if configure await is true. If false then they will run in parallel on separate threads.

So I think that the progress bar that I am updating in the while loop is incorrect because - by the time the code reaches the while block, most of the initial for loop iterations will have been already completed. I hope that I have understood correctly so far.

I have the following options to report progress from inside the task:

  1. using IProgress (I think this is more suitable to report progress from another thread [for example when using Task.Run], or in usual async await if the configure await is false, resulting in code below the await to run in separate thread otherwise it will not show the progress bar moving as the ui thread will be blocked running the callbacks. In my current example code always runs on the same UI thread). So I was thinking the below point may be more appropriate solution.

  2. making the Task non-static so that I can access the progress bar from within the Task and do porgressbar1.PerformStep().

Another thing I have noticed is that await WhenAll doesn't guarantee that IProgress is fully executed.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
variable
  • 8,262
  • 9
  • 95
  • 215
  • Why is there an `await finishedTask;`? It's already `finished` – asaf92 Aug 04 '21 at 14:06
  • Just kept it as a provision to consume output of Task (for future use). – variable Aug 04 '21 at 15:35
  • 2
    In my personal opinion, this comes close to an denial of service attack. You said, you expect 10000 rows in your DataTable. You start with FuncAsync thousands of requests to a http service in a few milliseconds. I don't think that any service is capable of servicing those requests. Or am I wrong? – Steeeve Aug 04 '21 at 18:53
  • When do you want to report your progress? When the http call finishes or when the entire `FuncAsync` completes? If it is the latter, then they way you are reporting, bar some of the inefficiencies with the `while` loop, is correct. Imagine the entire `FuncAsync` was synchronous and completes within a few ms, what do you expect the progress to be? – JohanP Aug 04 '21 at 22:22
  • I want it updated at end of try block – variable Aug 05 '21 at 05:56

2 Answers2

7

The IProgress<T> implementation offered natively by the .NET platform, the Progress<T> class, has the interesting characteristic of notifying the captured SynchronizationContext asynchronously, by invoking its Post method. This characteristic sometimes results to unexpected behavior. For example can you guess what effect has the code below to the Label1 control?

IProgress<string> progress = new Progress<string>(s => Label1.Text = s);

progress.Report("Hello");

Label1.Text = "World";

What text will be eventually written to the label, "Hello" or "World"? The correct answer is: "Hello". The delegate s => Label1.Text = s is invoked asynchronously, so it runs after the execution of the Label1.Text = "World" line, which is invoked synchronously.

Implementing a synchronous version of the Progress<T> class is quite trivial. All you have to do is copy-paste Microsoft's source code, rename the class from Progress<T> to SynchronousProgress<T>, and change the line m_synchronizationContext.Post(... to m_synchronizationContext.Send(.... This way every time you invoke the progress.Report method, the call will block until the invocation of the delegate on the UI thread is completed. The unfortunate implication of this is that if the UI thread is blocked for some reason, for example because you used the .Wait() or the .Result to wait synchronously for the task to complete, your application will deadlock.

The asynchronous nature of the Progress<T> class is rarely a problem in practice, but if you want to avoid thinking about it you can just manipulate the ProgressBar1 control directly. After all you are not writing a library, you are just writing code in the event handler of a button to make some HTTP requests. My suggestion is to forget about the .ConfigureAwait(false) hackery, and just let the main workflow of your asynchronous event handler to stay on the UI thread from start to end. If you have synchronous blocking code that needs to be offloaded to a ThreadPool thread, use the Task.Run method to offload it. To create your tasks, instead of manually adding tasks to a List<Task>, use the handly LINQ Select operator to project each DataRow to a Task. Also add a reference to the System.Data.DataSetExtensions assembly, so that the DataTable.AsEnumerable extension method becomes available. Finally add a throttler (a SemaphoreSlim), so that your application makes efficient use of the available network bandwidth, and it doesn't overburden the target machine:

private async void Button1_Click(object sender, EventArgs e)
{
    Button1.Enabled = false;
    const int maximumConcurrency = 10;
    var throttler = new SemaphoreSlim(maximumConcurrency, maximumConcurrency);

    DataTable dataTable = (DataTable)GridView1.DataSource;
    ProgressBar1.Minimum = 0;
    ProgressBar1.Maximum = dataTable.Rows.Count;
    ProgressBar1.Step = 1;
    ProgressBar1.Value = 0;

    Task[] tasks = dataTable.AsEnumerable().Select(async row =>
    {
        await throttler.WaitAsync();
        try
        {
            await Task.Delay(3000); // Simulate an asynchronous HTTP request
            await Task.Run(() => Thread.Sleep(2000)); // Simulate synchronous code
        }
        catch
        {
            await Task.Run(() => Thread.Sleep(1000)); // Simulate synchronous code
        }
        finally
        {
            throttler.Release();
        }
        ProgressBar1.PerformStep();
    }).ToArray();

    await Task.WhenAll(tasks);
    Button1.Enabled = true;
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Won't you still create ten-thousands of tasks this way, all (minus concurrently allowed) of them waiting for the semaphore? I would tend to use the Parallel class in this scenario. – Steeeve Aug 05 '21 at 09:24
  • 1
    @Steeeve correct. The throttling solution presented in this answer is simple, but not the most efficient. Each incomplete `Task` created by the `SemaphoreSlim.WaitAsync` weighs around 200 bytes (300 when a `CancellationToken` is supplied), so for 10,000 rows you'll tax your RAM with ~2 MB of avoidable overhead. There is no performance penalty though (CPU-wise). For a more memory-efficient use of the `SemaphoreSlim` class you can take a look at [this answer](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations/10810730#10810730) by Theo Yaung. – Theodor Zoulias Aug 05 '21 at 09:52
  • @Steeeve regarding the `Parallel` class, currently it does not support asynchronous delegates. In a few months the new API [`Parallel.ForEachAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync?view=net-6.0) will become available, with the release of the .NET 6. You can check out [this question](https://stackoverflow.com/questions/68544324/is-parallel-foreachasync-a-replacement-to-a-plain-for-loop-append-to-task-list) for a brief review of this new API. – Theodor Zoulias Aug 05 '21 at 10:00
  • 1
    thanks for the details. With this background info is clear to me, that using the synchronous Parallel class wouldn't be better. – Steeeve Aug 05 '21 at 10:16
  • Theodor - this line -> `dataTable.AsEnumerable().Select......ToArray()`. So there is 1. As enumerates call, 2. Select, 3. ToArray all. Please can you tell me what these concepts are so I can read more about them. – variable Aug 05 '21 at 13:23
  • @variable this is LINQ, and specifically [LINQ to Objects](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/linq-to-objects). It's a technology that allows to begin with an `IEnumerable`, and apply various transformations on it by chaining extension methods like `Select`, `Where` etc that are called "operators". Most of those transformations are lazy in nature (the term used is "deferred execution"). Finally the resulting `IEnumerable` is materialized by either enumerating it, or by attaching an operator that forces immediate evaluation, like the `ToArray`. – Theodor Zoulias Aug 05 '21 at 14:46
  • Theodor- suppose in place of thread.sleep, if I want to pass a data row column value into the task.run to perform some processin, then should I pass it as `somefn(dr[col])` or should I first store the value in `string val=somefn(dr[col]); somefn(val)`? I am asking because I feel task.run will be impacted by closure in the sense that all tasks will run on the last iterated value otherwise. – variable Aug 21 '21 at 04:12
  • @variable yes, `for` loops in general are affected be the [Captured variable in a loop in C#](https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp) issue. `foreach` loops have not the same problem. – Theodor Zoulias Aug 21 '21 at 07:16
  • Stupid question but what kind of control is the ProgressBar1? Is it under the System.Web.UI.WebControls namespace? – Kounavi Jan 05 '23 at 14:31
  • 1
    @Kounavi it's a [System.Windows.Forms.ProgressBar](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.progressbar). – Theodor Zoulias Jan 05 '23 at 15:19
-1

You can simply add a wrapper function:

private IProgress<double> _progress;
private int _jobsFinished = 0;
private int _totalJobs = 1000;

private static async Task FuncAsync()
{
    try
    {
        await Task.Delay(3000); //assume this is an async http post request that takes 3 seconds to respond

        Thread.Sleep(1000); //assume this is some synchronous code that takes 2 second
    }
    catch (Exception e)
    {
        Thread.Sleep(1000); //assume this is synchronous code that takes 1 second
    }
}

private async Task AwaitAndUpdateProgress()
{
    await FuncAsync(); // Can also do Task.Run(FuncAsync) to run on a worker thread
    _jobsFinished++;
    _progress.Report((double) _jobsFinished / _totalJobs);
}

And then just WhenAll after adding all the calls.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
asaf92
  • 1,557
  • 1
  • 19
  • 30