2

SHORT VERSION:

Can anyone confirm, does Task.Run/Task.WaitAll on non-async sub-functions with EF calls gain anything for ASP.NET apps?

LONG VERSION:

We have an Asp.Net WebApi service with a method which makes multiple DB calls (all using EntityFramework) to collect a set of data that all get returned into a single response object. We broke this up into a series of sub-functions, and wanted these to run in parallel (NOTE: None of these use any async calls).

To achieve some form of parallel coding, we invoke each function with Task.Run, followed by Task.WaitAll:

public ResponseObject PopulateResponseObject(int id)
{
    var response = new ResponseObject();

    Task<DataSetA> dataSetATask = Task.Run(() => getDataSetA(id));
    Task<DataSetB> dataSetBTask = Task.Run(() => getDataSetB(id));
    Task.WaitAll(dataSetATask, dataSetBTask);

    response.SetA = dataSetATask.Result;
    response.SetB = dataSetBTask.Result;

    return response;
}

From what I've been reading, Task.Run may not gain much in an Asp.Net application, and what we have here may just result in unnecessary thread-pool overhead.

Are we wasting time writing our code this way?

UPDATE: We are familiar with the fact that EF has async versions, but there is a significant amount of code that would need to be modified. We'd like to leave these functions as-is.

RunzWitScissors
  • 128
  • 1
  • 8
  • 1
    You can probably just use one of the `Parallel.ForEach` overloads to save some hassle – Marie May 28 '20 at 18:51
  • 1
    It depends on usage pattern. If you have endpoint which is called relatively rarely you can possibly improve performance (especially in case of some long-running stuff) but in general it [seems](https://stackoverflow.com/a/44149141/2501279) not being worth the hassle. – Guru Stron May 28 '20 at 19:00
  • 1
    That will be using a lot more threads than needed. `async-await` was created to save thread usage. EF has asynchronous APIs. – Paulo Morgado May 28 '20 at 19:12
  • @Marie - Good suggestion. I just implemented a Parallel.Invoke version and am testing that. – RunzWitScissors May 28 '20 at 21:05
  • 2
    It's impossible to tell whether running these in parallel will yield any benefit when the system is under load. If you are handling requests from many users, threads could easily be a scarce resource and you may end up waiting for one to become available. You might also experience blocking between threads due to concurrency locks. The only way to tell if there is overall benefit is to simulate the expected load and see how the system responds. Also, see [race your horses](https://ericlippert.com/2012/12/17/performance-rant/). – John Wu May 28 '20 at 21:22

3 Answers3

2

using Task.Run at least lets you run the two queries in parallel, so yes there is improvement to be gained (vs the totally synchronous version where you run the queries sequentially).

However, the gain is limited to the number of threads you have in threadpool (which is ultimately limited by your server capacity). You'll get more gain by using the EF async apis because that will allow many queries to be executed without tieing up a thread for each query.

But this is a reasonable strategy to start using async code. Once you've done this, you can come back and slowly convert the queries to use the async api.

Brandon
  • 38,310
  • 8
  • 82
  • 87
2

Based on my experiences, I strongly recommend you never kill simplicity for insignificant performance gain. Impact of this parallelism seems to me insignificant with huge development, maintenance and debugging burden.

Specially because you use EF and interacting with DB, probably you'll face to many concurrency issues that could be mare! also I suggest to take a look at this.

Arman Ebrahimpour
  • 4,252
  • 1
  • 14
  • 46
0

After doing some performance testing against Sync, Async (using the method above), and Parallel.Invoke, I found some interesting results. The parallel version was similar to:

public ResponseObject PopulateResponseObject(int id)
{
    var response = new ResponseObject();

    Parallel.Invoke(
        () => response.SetA = getDataSetA(id),
        () => response.SetB = getDataSetB(id),
        );

    return response;
}

And the Async appears above, in my original post.

It turns out that on average, compared to basic Sync calls, Async calls (Task.Run/Task.WaitAll) ran in 57% of the time, and Parallel.Invoke ran in 65% of the time.

So there was an advantage of using Task.Run/WaitAll in an ASP.NET app.

UPDATE: BTW this testing was done by pushing our code to our test environment where our QA folks were performing tests. It certainly doesn't mimic our prod environment, but does show it's slightly better.

RunzWitScissors
  • 128
  • 1
  • 8