-2

I know that Task.Run is used for CPU Bound operations but I find it quite hard to understand when to use it in a scenario where you mix CPU and I/O bound operations. For example:

This is a little helper function which does some CPU work and also makes an http request:

public async Task<string> MakeHttpRequest(HttpRequestMessage request)
{
   //before that do some CPU bound operations for example preparing the 
   // request before sending it out or validating the request object
   var HttpClient = new HttpClient();
   var response = await HttpClient.SendAsync(request);
   var responseString = await response.Content.ReadAsStringAsync();
   return responseString;
}

Now I need to make multiple calls with that method and I want to parallelize it so I get better performance out of my application. For that I generate a list of tasks with the method calls like this:

//Variant 1
public List<Task<string>> GenerateTasks()
{
   HttpRequestMessage request = new HttpRequestMessage(); //...
   List<Task<string>> taskList = new()
   {
      MakeHttpRequest(request),
      MakeHttpRequest(request)
   };
   return taskList;
}

//Variant 2
public List<Task<string>> GenerateTasks2()
{
   HttpRequestMessage request = new HttpRequestMessage(); //...
   List<Task<string>> taskList = new()
   {
      Task.Run(() => MakeHttpRequest(request)),
      Task.Run(() => MakeHttpRequest(request))
   };
   return taskList;
}

//Variant 3 - There is even this:
public List<Task<string>> GenerateTasks3()
{
   HttpRequestMessage request = new HttpRequestMessage(); //...
   List<Task<string>> taskList = new()
   {
      Task.Run(async () => await MakeHttpRequest(request)),
      Task.Run(async () => await MakeHttpRequest(request))
   };
   return taskList;
}

At the end I would just do an await Task.WhenAll(GenerateTasks()). Pls note the lack of exception handling etc, it is just an example.

What is the difference between Variant 1,2 and 3? Since I am doing CPU Bound operations before the I/O operation would it be ok to use Task.Run or not? Are there any negative side effects if it is not ok to use Task.Run like this?

Florent
  • 111
  • 10
  • Do we have a UI Context here? – Fildor Mar 13 '23 at 09:16
  • There is nothing about `Task.Run` that is specifically for CPU bound operations. Why do you think that? – Enigmativity Mar 13 '23 at 10:03
  • Your code isn't real. When I copy and pasted it into a console app I get a lot of errors. Can you please post real code? – Enigmativity Mar 13 '23 at 10:05
  • @Fildor There is no UI Context, we are in a web api – Florent Mar 13 '23 at 11:15
  • @Enigmativity I have read this article about Task.Run and CPU bound operations: https://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html Also it is not about the rightfullness of the code, I want to know the different effects of the GenerateTasks method. – Florent Mar 13 '23 at 11:16
  • @Florent - You missed the key phrase "in an asynchronous way". It's like saying "surgery is all about cutting people open" versus "surgery is all about cutting people open in order to perform a life-saving operation". – Enigmativity Mar 13 '23 at 20:39
  • @Florent - Can you please fix your code? I'd like to copy, paste, and run your code. At least to the point that it compiles. – Enigmativity Mar 13 '23 at 20:40
  • 1
    @Enigmativity What is not working for you? Just copy those 4 methods to a console app and fix some trivial compile errors. Also set an URI for the request object. Is that hard or am I missing something? – Florent Mar 14 '23 at 07:18
  • @Florent - Yes, "Just copy those 4 methods to a console app and fix some trivial compile errors. Also set an URI for the request object." is exactly what I'm asking you to do. It's not our job to get the question to an easily answerable state. We're here to answer questions, not help ask them. – Enigmativity Mar 14 '23 at 10:35
  • 1
    @Enigmativity My question was about the effect of the code. There is no statement of me saying: "Try this code out". Again it is about the effect not about the ability to run the code. The effect of this example can be determined by people who actually are experienced when it comes to concurrency/parallelism. Theodor wrote exactly the answer that I was looking for. But still thanks for your effort. – Florent Mar 14 '23 at 11:36
  • @Florent - Please understand that when someone has in excess of 100k rep point on here that they know what they're talking about. You're lucky that someone as experienced as Theo took the time to answer, but when you under cook the question then you risk not getting good answers. I'm trying to help you to get the maximum benefit from your time here. – Enigmativity Mar 14 '23 at 11:48
  • @Enigmativity in some questions, including a minimal and reproducible example is helpful and desirable. For example when the question asks to explain an unexpected exception or some other bizarre behavior. In other questions a minimal reproducible example doesn't offer much value. For example in this question, if someone doesn't already know the answer, they are too far from answering it anyway. No amount of experimentation with a runnable piece of code will allow them to learn enough about the subject, so that they can post a useful answer. I posted myself such a question today. – Theodor Zoulias Mar 14 '23 at 12:37
  • @TheodorZoulias - I disagree - if I'm to refactor code then I want something that compiles. I don't like posting answers that don't run (or at the very least compile) so I don't want to have to fix the OP's code in order to answer. – Enigmativity Mar 15 '23 at 00:12

2 Answers2

1

The Variant 2 and Variant 3 are practically identical, so I'll compare only the variant 1 (without Task.Run) versus the variant 2 (with Task.Run).

  1. Without Task.Run the MakeHttpRequest method is invoked synchronously on the current thread. The two Task<string> tasks are created sequentially, the one after the other. In other words the creation of the tasks is not parallelized.

  2. With Task.Run the MakeHttpRequest method is invoked in parallel on ThreadPool threads. The two Task<string> tasks are created concurrently and in parallel.

The first variant is what Stephen Cleary calls asynchronous concurrency, distinguishing it from the parallelism, which is what the second variant does. The term "asynchronous concurrency" is not well defined though. It might mean that we avoid scheduling work on the ThreadPool pool (without actively enforcing a no-parallelization policy), or it might mean that we take measures to ensure that only one thread will be running code at any time, by using either a SynchronizationContext or an exclusive TaskScheduler. The variant 1 implements the first interpretation. Although the code before the await HttpClient.SendAsync(request) will be serialized (because it is called synchronously), any CPU-bound operation after the await might be parallelized.

It might be useful to compare the two variants with the native .NET 6 Parallel.ForEachAsync API. This method behaves similarly to your variant 2 (with Task.Run). A proposal to enable the variant 1 behavior (without Task.Run) has been rejected by Microsoft.

Both variants are valid, and in different scenarios the one might be more suitable than the other. For example in ASP.NET applications the variant 1 (without Task.Run) might be preferable, because the ASP.NET infrastructure uses the same ThreadPool for scheduling web requests, so we want to avoid putting additional stress to the ThreadPool by scheduling "parasitic" work to it. Quoting from an MSDN article.

You can kick off some background work by awaiting Task.Run, but there’s no point in doing so. In fact, that will actually hurt your scalability by interfering with the ASP.NET thread pool heuristics. If you have CPU-bound work to do on ASP.NET, your best bet is to just execute it directly on the request thread. As a general rule, don’t queue work to the thread pool on ASP.NET.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

You are not really showing any example of any CPU bound operations. If you want to do multiple async operations concurrently, just start both and then await.

var request1 = MakeHttpRequest(uri1);
var request2 = MakeHttpRequest(uri2);
var result1 = await request1 ;
var result2 = await request2 ;

if you want to do some CPU-bound processing you would need to use Task.Run, but if you want to ensure it is started when either request completes you may need an intermediate method:

var result1Task = HandleResult(MakeHttpRequest(uri1));
var result2Task = HandleResult(MakeHttpRequest(uri2));
var result1 = await result1Task ;
var result2 = await result2Task ;

...
public async Task<MyResult> HandleResult(Task<string> request){
   var str = await request;
   return await Task.Run(() => MyCPUBoundMethod(str));
}
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • 1
    Why await both? Why just not use await Task.Whenall(request1, request2)? With multiple awaits the compilers needs to generate way more async state machine code or not? – Florent Mar 13 '23 at 11:39
  • @Florent You could absolutely use `WhenAll`. But I do not think there will be a significant difference in overhead. Sure, you will need an extra state, but that should not be expensive. Using two awaits should be easier to read, at least as long as there are only two tasks. – JonasH Mar 13 '23 at 12:00
  • There are 6 in total. Two of them need to be completed before the other 4 can execute. Also the 4 other tasks have multiple request in them because of dependencies. Is there a rule of thumb where you decide if you do 2 tasks for 2 requests or 1 tasks that does both requests? – Florent Mar 13 '23 at 12:09
  • @Florent I'm not sure what your point is. While async code has some overhead, it is generally assumed that whatever you are calling is slow enough that the overhead is relatively minor. A point with this solution is that if request2 completes before request1, it should run `MyCPUBoundMethod` immediately, not wait for request1 to complete. There are absolutely other ways to achieve the same thing, but I hope that this is one of the more readable alternatives. But as always, if performance is critical you might need to sacrifice readability. – JonasH Mar 13 '23 at 12:31