ChatGPT解决这个技术问题 Extra ChatGPT

Wrapping synchronous code into asynchronous call

I have a method in ASP.NET application, that consumes quite a lot of time to complete. A call to this method might occur up to 3 times during one user request, depending on the cache state and parameters that user provides. Each call takes about 1-2 seconds to complete. The method itself is synchronous call to the service and there is no possibility to override the implementation. So the synchronous call to the service looks something like the following:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

And the usage of the method is (note, that call of method may happen more than once):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

I tried to change the implementation from my side to provide simultaneous work of this method, and here is what I came to so far.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Usage (part of "do other stuff" code runs simultaneously with the call to service):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

My question is the following. Do I use the right approach to speed up the execution in ASP.NET application or am I doing unnecessary job trying to run synchronous code asynchronously? Can anyone explain why the second approach is not an option in ASP.NET (if it is really not)? Also, if such approach is applicable, do I need to call such method asynchronously if it is the only call we might perform at the moment (I have such case, when no other stuff there is to do while waiting for completion)?
Most of the articles in the net on this topic covers using async-await approach with the code, that already provides awaitable methods, but that's not my case. Here is the nice article describing my case, which doesn't describe the situation of parallel calls, declining the option to wrap sync call, but in my opinion my situation is exactly the occasion to do it.
Thanks in advance for help and tips.


S
Stephen Cleary

It's important to make a distinction between two different types of concurrency. Asynchronous concurrency is when you have multiple asynchronous operations in flight (and since each operation is asynchronous, none of them are actually using a thread). Parallel concurrency is when you have multiple threads each doing a separate operation.

The first thing to do is re-evaluate this assumption:

The method itself is synchronous call to the service and there is no possibility to override the implementation.

If your "service" is a web service or anything else that is I/O-bound, then the best solution is to write an asynchronous API for it.

I'll proceed with the assumption that your "service" is a CPU-bound operation that must execute on the same machine as the web server.

If that's the case, then the next thing to evaluate is another assumption:

I need the request to execute faster.

Are you absolutely sure that's what you need to do? Are there any front-end changes you can make instead - e.g., start the request and allow the user to do other work while it's processing?

I'll proceed with the assumption that yes, you really do need to make the individual request execute faster.

In this case, you'll need to execute parallel code on your web server. This is most definitely not recommended in general because the parallel code will be using threads that ASP.NET may need to handle other requests, and by removing/adding threads it will throw the ASP.NET threadpool heuristics off. So, this decision does have an impact on your entire server.

When you use parallel code on ASP.NET, you are making the decision to really limit the scalability of your web app. You also may see a fair amount of thread churn, especially if your requests are bursty at all. I recommend only using parallel code on ASP.NET if you know that the number of simultaneous users will be quite low (i.e., not a public server).

So, if you get this far, and you're sure you want to do parallel processing on ASP.NET, then you have a couple of options.

One of the easier methods is to use Task.Run, very similar to your existing code. However, I do not recommend implementing a CalculateAsync method since that implies the processing is asynchronous (which it is not). Instead, use Task.Run at the point of the call:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Alternatively, if it works well with your code, you can use the Parallel type, i.e., Parallel.For, Parallel.ForEach, or Parallel.Invoke. The advantage to the Parallel code is that the request thread is used as one of the parallel threads, and then resumes executing in the thread context (there's less context switching than the async example):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

I do not recommend using Parallel LINQ (PLINQ) on ASP.NET at all.


Thanks a lot for a detailed answer. One more thing I want to clarify. When I start execution using Task.Run, the content is ran in another thread, that is taken from the thread pool. Then there is no need at all to wrap blocking calls (such as mine call to service) into Runs, because they will always consume one thread each, that will be blocked during execution of the method. In such situation the only benefit that is left from async-await in my case is performing several actions at a time. Please, correct me if I'm wrong.
Yes, the only benefit you're getting from Run (or Parallel) is concurrency. The operations are still each blocking a thread. Since you say the service is a web service, then I do not recommend using Run or Parallel; instead, write an asynchronous API for the service.
@StephenCleary. what do you mean by "...and since each operation is asynchronous, none of them are actually using a thread..." thanks!
@alltej: For truly asynchronous code, there is no thread.
@JoshuaFrank: Not directly. Code behind a synchronous API must block the calling thread, by definition. You can use an asynchronous API like BeginGetResponse and start several of those, and then (synchronously) block until all of them are complete, but this kind of approach is tricky - see blog.stephencleary.com/2012/07/dont-block-on-async-code.html and msdn.microsoft.com/en-us/magazine/mt238404.aspx . It's usually easier and cleaner to adopt async all the way, if possible.
k
kernowcode

I found that the following code can convert a Task to always run asynchronously

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

and I have used it in the following manner

await ForceAsync(() => AsyncTaskWithNoAwaits())

This will execute any Task asynchronously so you can combine them in WhenAll, WhenAny scenarios and other uses.

You could also simply add the Task.Yield() as the first line of your called code.


d
david.barkhuizen

this is probably the easiest generic way in your case

return await new Task(
    new Action(
        delegate () { 
          // put your synchronous code here
        }
    )
);

Not bad, but why is this 3-level nesting answer simpler than the 2014 answer with one line: var task = Task.Run(() => Calculate(myInput)); ?