Search code examples
c#task-parallel-library.net-4.5

Run multiple actions in parallel in C#


I have the below code in a ASP.Net web forms page which basically checks if some value is in cache and if not it calls a method which gets the data and then stores it in Cache. The method which gets the data is below

ChartRenderingHelper.GenerateBidsStatusCreated(currYear.ToString(), currQuarter.ToString(), currYearType.ToString())

calls a Stored proc using EF and these 3 calls all call separate SP's..Now I am doing it sequentially so if each SP takes 5 seconds the total operation takes 15..I was thinking of using some Tasks to run these in parallel but not sure how I can change my current code to do that.

  bidsCreated.Value = DashboardCacheHelper.IsIncache(bidsCreatedKey, useCaching) ? DashboardCacheHelper.GetFromCache(bidsCreatedKey) : (string)DashboardCacheHelper.SaveCache(bidsCreatedKey, JsonConvert.SerializeObject(ChartRenderingHelper.GenerateBidsStatusCreated(currYear.ToString(), currQuarter.ToString(), currYearType.ToString())), DateTime.Now.AddDays(cacheDays));
            bidsSubmitted.Value = DashboardCacheHelper.IsIncache(bidsSubmittedKey, useCaching) ? DashboardCacheHelper.GetFromCache(bidsSubmittedKey) : (string)DashboardCacheHelper.SaveCache(bidsSubmittedKey, JsonConvert.SerializeObject(ChartRenderingHelper.GenerateBidsStatusSubmitted(currYear.ToString(), currQuarter.ToString(), currYearType.ToString())), DateTime.Now.AddDays(cacheDays));
            bidsClosed.Value = DashboardCacheHelper.IsIncache(bidsClosedKey, useCaching) ? DashboardCacheHelper.GetFromCache(bidsClosedKey) : (string)DashboardCacheHelper.SaveCache(bidsClosedKey, JsonConvert.SerializeObject(ChartRenderingHelper.GenerateBidsStatusClosed(currYear.ToString(), currQuarter.ToString(), currYearType.ToString())), DateTime.Now.AddDays(cacheDays));

How can I do these 3 assignments in parallel? Using TPL I know we can run methods in parallel

Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());

Is this the recommended approach and if I have say 12 operations that need to run in parallel all calling SQL Stored Procs using EF would that be an issue in performance.


Solution

  • You can use asynchrony to achieve this, however it's best to have an asynchronous implementation of your Entity Framework code for adding and fetching.

    Firstly, I have refactored the code to reduce duplication and make it more readable by introducing a method GetOrAddAsync, this method accepts the cache key, a boolean for useCaching (what ever this is for?) and a delegate for the ChartRenderingHelper methods. It's not immediately clear what type currQuarter and currYearType are.

    private async Task<string> GetOrAddAsync(string cacheKey, bool useCaching, int cacheDays, DateTime currYear, ? currQuarter, ? currYearType, Action<string, string, string> cacheFactory)
    {
        if(DashboardCacheHelper.IsIncache(cacheKey, useCaching))
        {
            return DashboardCacheHelper.GetFromCache(cacheKey);
        }
    
        return (string)(await DashboardCacheHelper.SaveCacheAsync(bidsClosedKey, JsonConvert.SerializeObject(cacheFactory(currYear.ToString(), currQuarter.ToString(), currYearType.ToString())), DateTime.Now.AddDays(cacheDays)).ConfigureAwait(false))
    }
    

    With the above method, your assignment code then becomes

    var bidsCreatedCacheTask = GetOrAddAsync(bidsCreatedKey, useCaching, cacheDays, currYear, currQuarter, currYearType, ChartRenderingHelper.GenerateBidsStatusCreated);
    var bidsSubmittedCacheTask = GetOrAddAsync(bidsSubmittedKey, useCaching, cacheDays, currYear, currQuarter, currYearType, ChartRenderingHelper.GenerateBidsStatusSubmitted);
    var bidsClosedCacheTask = GetOrAddAsync(bidsClosedKey, useCaching, cacheDays, currYear, currQuarter, currYearType, ChartRenderingHelper.GenerateBidsStatusClosed);
    
    await Task.WhenAll(bidsCreatedCacheTask, bidsSubmittedCacheTask, bidsClosedCacheTask).ConfigureAwait(false);
    
    bidsCreated.Value = await bidsCreatedCacheTask;
    bidsSubmitted.Value = await bidsSubmittedCacheTask;
    bidsClosed.Value = await bidsClosedCacheTask;
    

    One key thing to remember is to mark your calling method as async, if it's an event handler then it'll be async void - but please bare in mind that async void should otherwise be avoided in any method that isn't an event handler - because you're constrained to a void return type with an event handler.

    As LeBigCat has pointed out in the comments, ConfigureAwait should be used with asynchronous code that runs on .NET Framework to prevent deadlocks, read here to see why.