29

I'm looking for the proper way to handle multiple database calls that would likely benefit from running simultaneously. The queries are just to stored procedures that are either doing inserts or merges using data that is programmatically assembled into DataTables in my ASP.NET MVC app.

Of course I have seen some information on async and await, and that appears to be what I would need to do, but I don't have a clear understanding of how to implement it. Some information is saying that the calls would still be sequential, and that one would still be waiting on another to complete. That seems pointless.

Ultimately, I would like a solution that allows me to run all the queries in the time it takes for the longest procedure to complete. I would like all the queries to return the number of records affected (as they do now) as well.

Here is what I have going on now (which is in no way parallel):

// Variable for number of records affected
var recordedStatistics = new Dictionary<string, int>();

// Connect to the database and run the update procedure
using (var dbc = new SqlConnection(db.Database.Connection.ConnectionString))
{
    dbc.Open();

    // Merge One procedure
    using (SqlCommand cmd = new SqlCommand("MergeOneProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeOneDataTable);

        // Execute procedure and record the number of affected rows
        recordedStatistics.Add("mergeOne", cmd.ExecuteNonQuery());
    }

    // Merge Two procedure
    using (SqlCommand cmd = new SqlCommand("MergeTwoProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeTwoDataTable);

        // Execute procedure and record the number of affected rows
        recordedStatistics.Add("mergeTwo", cmd.ExecuteNonQuery());
    }

    // Merge Three procedure
    using (SqlCommand cmd = new SqlCommand("MergeThreeProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeThreeDataTable);

        // Execute procedure and record the number of affected rows
        recordedStatistics.Add("mergeThree", cmd.ExecuteNonQuery());
    }

    // Merge Four procedure
    using (SqlCommand cmd = new SqlCommand("MergeFourProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeFourDataTable);

        // Execute procedure and record the number of affected rows
        recordedStatistics.Add("mergeFour", cmd.ExecuteNonQuery());
    }

    // Merge Five procedure
    using (SqlCommand cmd = new SqlCommand("MergeFiveProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeFiveDataTable);

        // Execute procedure and record the number of affected rows
        recordedStatistics.Add("mergeFive", cmd.ExecuteNonQuery());
    }

    dbc.Close();
}

return recordedStatistics;

All of that code is within the same method that assembles the data for the DataTables. My limited understanding of async would lead me to believe that I would need to extract the previous code into its own method. I would then call that method and await the return. However, I don't even know enough about it to begin.

I have never done any asynchronous/parallel/multithreaded coding before. This situation just makes me feel like it is the perfect time to jump in. That said, I would like to learn the best way, instead of having to unlearn the wrong way.

FlipperBizkut
  • 423
  • 1
  • 5
  • 15

1 Answers1

24

Here is an example of how you would do it:

Here I am creating two methods to wrap two operations, you need to do the same for the other operations:

public async Task<int> MergeOneDataTableAsync()
{
    // Merge One procedure
    using (SqlCommand cmd = new SqlCommand("MergeOneProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeOneDataTable);

        return await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
    }
}


public async Task<int> MergeTwoDataTableAsync()
{
    // Merge Two procedure
    using (SqlCommand cmd = new SqlCommand("MergeTwoProcedure", dbc))
    {
        // 5 minute timeout on the query
        cmd.CommandTimeout = 300;
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("@TVP", MergeTwoDataTable);

        return await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
    }
}

Notice that I am using the ExecuteNonQueryAsync method to execute the query.

And then your original method would look like this:

using (var dbc = new SqlConnection(db.Database.Connection.ConnectionString))
{
    dbc.Open();

    Task<int> task1 = MergeOneDataTableAsync();
    Task<int> task2 = MergeTwoDataTableAsync();

    Task.WaitAll(new Task[]{task1,task2}); //synchronously wait

    recordedStatistics.Add("mergeOne", task1.Result);
    recordedStatistics.Add("mergeTwo", task2.Result);
}

Please note that I am keeping this method synchronous. Another option (actually a better one) is to convert the method into an asynchronous one like this:

public async Task<Dictionary<string, int>> MyOriginalMethod()
{
    //...
    using (var dbc = new SqlConnection(db.Database.Connection.ConnectionString))
    {
        dbc.Open();

        Task<int> task1 = MergeOneDataTableAsync();
        Task<int> task2 = MergeTwoDataTableAsync();

        int[] results = await Task.WhenAll(new Task<int>[]{task1,task2});

        recordedStatistics.Add("mergeOne", results[0]);
        recordedStatistics.Add("mergeTwo", results[1]);
    }

    //...
    return recordedStatistics;
}

But this would mean that you have to invoke it asynchronously (async all the way).

Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
  • That looks great. It is, in fact, a GUI application, but I expect it to lock up the GUI until the queries have finished. Once they are done, I load up a view with the number of affected records from each of the queries. I want/need that information, so I either baby-sit the calling page, or I come back to it once I have given it a minute to complete. I just didn't see the point in waiting 5 minutes per query, one after the other, when I can wait the same 5 minutes to have all of them complete at once. Thanks! – FlipperBizkut Feb 13 '16 at 01:15
  • 6
    You've actually got a deadlock waiting to happen calling `Task.WaitAll` on the tasks created by `MergeOneDataTableAsync` and `MergeTwoDataTableAsync`, which both use `await` without `ConfigureAwait(false)`. – Kirill Shlenskiy Feb 13 '16 at 01:15
  • 1
    Could you elaborate on the deadlock issue? Perhaps show a way to avoid it as well? – FlipperBizkut Feb 13 '16 at 01:16
  • @KirillShlenskiy, that is true for the synchronous version. That is why I warned OP about it. – Yacoub Massad Feb 13 '16 at 01:16
  • Here is another question... Instead of creating 5 different methods (one for each call), could I create one that accepts some arguments (such as the DataTable and the name of the stored procedure) and call it 5 times? – FlipperBizkut Feb 13 '16 at 01:20
  • @FlipperBizkut, you definitely have the right idea about making your @TVP `DataTable` a parameter. Go for it. – Kirill Shlenskiy Feb 13 '16 at 01:22
  • Thanks @YacoubMassad and @KirillShlenskiy! I appreciate the help! – FlipperBizkut Feb 13 '16 at 01:25
  • 1
    @FlipperBizkut, more info on what *could* cause the deadlock in the initial synchronous version of the code here: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html and here: http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115163.aspx – Kirill Shlenskiy Feb 13 '16 at 01:28
  • Is `.ConfigureAwait(false)` improving performance? – variable Jul 29 '21 at 19:35