0

I have 2 examples about mutating the data table inside of an async function. Since the DataTable and DataRow are of reference type, we can pass them into function and mutate - for example remove the row from the data table and this will effect the data table outside of the function. I know that doing so is unsafe in a multi threaded environment.

However this question pertains to the use of async await without making use of Task.Run - so effectively we always have 1 thread - the UI thread running the code.

Example 1-

   private static async Task FuncAsync(DataTable dt, DataRow dr)
    {
        try
        {
            await Task.Delay(2000); //some io bound work here
            
            Thread.Sleep(2000); //some syncronous blocking code here like log to db
            dt.Rows.Remove(dr); //mutate datatable - remove the datarow from the datatable if there is no exception // this can be add/delete or even updating of the row
        }
        catch (Exception e)
        {
            Thread.Sleep(2000); //some syncronous blocking code here like log to db
        }
    }

    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(dt, dr);                
        }            

        while (lstTasks.Any())
        {   
            Task finishedTask = await Task.WhenAny(lstTasks);
            lstTasks.Remove(finishedTask);
            await finishedTask;
            progressbar1.ReportProgress();
        }
        
        MessageBox.Show("Rows that resulted in exception: " + dt.Rows.Count.ToString());
    }
}

Example 2-

private void Form1_Load(object sender, EventArgs e)
        {
            DataTable dt = new DataTable();
            dt.Columns.Add("col1");
            for (int i =0; i < 1000; i++)
            {
                dt.Rows.Add(i);
            }

            dataGridView1.DataSource = dt; //Create datatable with 999 rows
        }

        private async Task FnMutateDtAsync(DataTable dt)
        {
            await Task.Delay(2000); //represents for io work
            
            bool tst = false; //code following this line is syncronous
            foreach (DataRow dr in dt.Rows)
            {
                if (dr["col1"].ToString() == "2") //if 2 exists then delete the 1st row
                {
                    tst = true;
                }
            }

            if (tst)
            {
                dt.Rows.RemoveAt(0); //mutate the datatable - this can be add/delete/modify value of cell
            }
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            List<Task> tsks = new List<Task>();
            for (int i = 0; i < 5; i++) //run 5 tasks async
            {
                tsks.Add(FnMutateDtAsync((DataTable)dataGridView1.DataSource));
            }

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

In both above examples, I am mutating the datatable from inside the async function.

  1. Is there any harm that I must expect when writing code like this? Can I consider this coding style to be thread safe since at any point only 1 thread is doing all the work?

  2. Is it safe to do ConfigureAwait(false) on the await line?

variable
  • 8,262
  • 9
  • 95
  • 215
  • Why wouldn't you use `Task.WhenAll()`, you are not using any return values of the tasks. – Jeroen van Langen Jul 30 '21 at 13:25
  • I have kept a provision so that I can update progress bar at the end of the while loop. Now updated code. – variable Jul 30 '21 at 13:30
  • What are you trying to do? A `DataTable` is an in-memory container. `Remove` is a CPU-bound operation that modifies the container, no different than a `List<>.Remove`. None of these methods perform any asynchronous operation. All of them modify the DataTable using the UI thread – Panagiotis Kanavos Jul 30 '21 at 13:32
  • I am just asking whether it is thread safe to mutate datatable inside async task. – variable Jul 30 '21 at 13:34
  • 1
    In any case, [DataTable](https://learn.microsoft.com/en-us/dotnet/api/system.data.datatable?view=net-5.0#thread-safety) isn't thread safe for modification so there's no way to modify it using multiple threads, no matter how they're constructed. – Panagiotis Kanavos Jul 30 '21 at 13:35
  • @variable you don't use any tasks. All operations run on the UI thread. Using `async` doesn't make a method run in the background. – Panagiotis Kanavos Jul 30 '21 at 13:35
  • @PanagiotisKanavos The question behind all this is, when he's using async methods, is it handle concurrently – Jeroen van Langen Jul 30 '21 at 13:36
  • @JeroenvanLangen bu in *this* case, there's nothing running in the background, except the timer used by `Task.Delay()`. – Panagiotis Kanavos Jul 30 '21 at 13:37
  • @PanagiotisKanavos yep, which is using the SynchronizationContext to post the remaining code (on the UI-thread). So the answer is: The code isn't executed concurrently – Jeroen van Langen Jul 30 '21 at 13:38
  • I have added an update asking for whether it is safe to do ConfigureAwait(false) on the await line? – variable Jul 30 '21 at 13:58
  • @variable again, what are you trying to do? If you use a background thread, you use a background thread, no matter how you got there. If you use `ConfigureAwait(false)` to continue on the timer's thread, you'll still be on the timer's background thread. DataTable isn't thread-safe. – Panagiotis Kanavos Jul 30 '21 at 14:05
  • @variable and once again, what's the point? Essentially you're trying to remove items from a `List`. There's nothing async about this. Are there so many rows to delete? Whatever the real problem is, there are far better ways of solving it. If you want to update the data from a background thread and *then* have the UI update itself, it's doable. For example, it's a common technique to pause UI updates while making lots of modifications and unpause them afterwards, to avoid flickering – Panagiotis Kanavos Jul 30 '21 at 14:08
  • You don't have any code that operates on the `DataTable` outside the UI thread, so **of course** it's safe. See duplicate for the details about async/await you should know. If you do some research, you could avoid a lot of trouble posting questions that have already been answered. – Peter Duniho Jul 30 '21 at 15:22
  • Linked as duplicate: [How and when to use ‘async’ and ‘await’](https://stackoverflow.com/questions/14455293/how-and-when-to-use-async-and-await) – Theodor Zoulias Jul 30 '21 at 17:18
  • Theodor - in the for loop, say there are 10000 iterations. As each iteration proceeds, say we have currently reached iteration number 4000. At that point a task from the initial iteration has completed its await Task.Delay. So for that task, the code below the await (inside that task) runs immediately or after the entire for loop completes? – – variable Aug 01 '21 at 12:49
  • Theodor - in example 1 line 5, support if I set configure await as false then will this ensure parallelism? Because code following await will run on separate thread. – variable Aug 01 '21 at 14:14

1 Answers1

2

In short: All the code is executed synchronously.


The UI-thread contains a WindowsFormsSynchronizationContext in it's SynchronizationContext, it will be used to post the remaining code after awaits of the 'async-statemachine' to the UI thread.

The problem you will be facing is that the Thread.Sleep(2000); is executed on the UI-thread, so it will block the UI-thread. Mostly db related actions do support async calling.

Another problem you might be facing is, when the dt.Rows.Remove(dr); is called before any await (so it is executed directly) you'll get a Collection was modified on the foreach (DataRow dr in dt.Rows)

Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • So it is safe to perform such mutation when not using Task.Run? I ask because I have read that Task.Run creates a new thread which would make it unsafe. – variable Jul 30 '21 at 13:32
  • Also I have updated the code by putting progress bar report call at the end of the while loop. – variable Jul 30 '21 at 13:33
  • Since it's abstracted away, you can't make assumptions about it – Jeroen van Langen Jul 30 '21 at 13:34
  • What do you mean by `dt.Rows.Remove(dr);` is called before any await? In the code example it is shown as called after await. – variable Jul 30 '21 at 13:49
  • 1
    That's why, if you would remove the await, the code will fail. To solve this, you should create a copy of the collection. (add `.ToList()` behind it). Because you're not allowed to modify the collection you are iterating – Jeroen van Langen Jul 30 '21 at 13:57
  • Is it safe to do ConfigureAwait(false) on the await line? – variable Jul 30 '21 at 13:57
  • If I am not allowed to modify the collection while iterating then why does using await allow me to do so? It will greatly help me to understand concept. Thank you – variable Jul 30 '21 at 14:02
  • @variable adding `ConfigureAwait(false)` in the mix will break the single-threaded model imposed by the `WindowsFormsSynchronizationContext`, and the behavior of your program will become undefined. – Theodor Zoulias Jul 30 '21 at 17:21
  • @variable Jeroen van Langen's comment regarding a possible "Collection was modified" exception does not refer to the current version of your program. It refers to a hypothetical version where the `dt.Rows.Remove(dr);` would be positioned before the first `await` inside the `FuncAsync`, or after an `await` of an already completed awaitable. The `Task.Delay(2000)` awaitable is extremely unlikely that could ever be completed at the `await` point, so currently your program is safe, but the window for bugs to creep in is not hermetically closed. – Theodor Zoulias Jul 30 '21 at 17:36
  • Theodor, why is it that normally I am not allowed to modify the collection while iterating whereas after using await why does it allow me to do so? – variable Jul 30 '21 at 18:15
  • Also, would it be safe if I replace .Remove with .Delete and then outside the loop do .AcceptChanges() – variable Jul 30 '21 at 18:37
  • Also, will setting ConfigureAwait as false on the await line prevent the Task from blocking the UI thread? – variable Aug 01 '21 at 15:23
  • The "collection was modified" will be thrown because the collection keeps track of a so called "version". If you do any modification on the collection, the version is increased. If you were still iterating the collection. it will compare if the list is still the same version. If not, it was modified. This check is made to prevent unwanted behavior, for example, should it iterate an item twice when you sort the list and the item gets moved? It is created so force programmers that a program is always executed in the same way. See it as an programming flaw detection. – Jeroen van Langen Aug 01 '21 at 18:37