Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manage many repetitive, CPU intensive tasks, running parallelly?

I need to constantly perform 20 repetitive, CPU intensive calculations as fast as possible. So there is 20 tasks which contain looped methods in :

while(!token.IsCancellationRequested) 

to repeat them as fast as possible. All calculations are performed at the same time. Unfortunatelly this makes the program unresponsive, so added :

await Task.Delay(15); 

At this point program doesn't hang but adding Delay is not correct approach and it unnecessarily slows down the speed of calculations. It is WPF program without MVVM. What approach would you suggest to keep all 20 tasks working at the same time? Each of them will be constantly repeated as soon as it finished. I would like to keep CPU (all cores) utilisation at max values (or near) to ensure best efficiency.

EDIT: There is 20 controls in which user adjusts some parameters. Calculations are done in:

private async Task Calculate()
{
   Task task001 = null;
   task001 = Task.Run(async () => 
   {
      while (!CTSFor_task001.IsCancellationRequested)
      {
          await Task.Delay(15);
          await CPUIntensiveMethod();
      }
   }, CTSFor_task001.Token);
}

Each control is independent. Calcullations are 100% CPU-bound, no I/O activity. (All values come from variables) During calculations values of some UI items are changed:

 this.Dispatcher.BeginInvoke(new Action(() =>
     {
          this.lbl_001.Content = "someString";
     }));
like image 968
as74 Avatar asked Mar 20 '23 12:03

as74


2 Answers

Let me just write the whole thing as an answer. You're confusing two related, but ultimately separate concepts (thankfully - that's why you can benefit from the distinction). Note that those are my definitions of the concepts - you'll hear tons of different names for the same things and vice versa.

Asynchronicity is about breaking the imposed synchronicity of operations (ie. op 1 waits for op 2, which waits for op 3, which waits for op 4...). For me, this is the more general concept, but nowadays it's more commonly used to mean what I'd call "inherent asynchronicity" - ie. the algorithm itself is asynchronous, and we're only using synchronous programming because we have to (and thanks to await and async, we don't have to anymore, yay!).

The key thought here is waiting. I can't do anything on the CPU, because I'm waiting for the result of an I/O operation. This kind of asynchronous programming is based on the thought that asynchronous operations are almost CPU free - they are I/O bound, not CPU-bound.

Parallelism is a special kind of the general asynchronicity, in which the operations don't primarily wait for one another. In other words, I'm not waiting, I'm working. If I have four CPU cores, I can ideally use four computing threads for this kind of processing - in an ideal world, my algorithm will scale linearly with the number of available cores.

With asynchronicity (waiting), using more threads will improve the apparent speed regardless of the number of the available logical cores. This is because 99% of the time, the code doesn't actually do any work, it's simply waiting.

With parallelism (working), using more threads is directly tied to the number of available work cores.

The lines blur a lot. That's because of things you may not even know are happening, for example the CPU (and the computer as a whole) is incredibly asynchronous on its own - the apparent synchronicity it shows is only there to allow you to write code synchronously; all the optimalizations and asynchronicity is limited by the fact that on output, everything is synchronous again. If the CPU had to wait for data from memory every time you do i ++, it wouldn't matter if your CPU was operating at 3 GHz or 100 MHz. Your awesome 3 GHz CPU would sit there idle 99% of the time.

With that said, your calculation tasks are CPU-bound. They should be executed using parallelism, because they are doing work. On the other hand, the UI is I/O bound, and it should be using asynchronous code.

In reality, all your async Calculate method does is that it masks the fact that it's not actually inherently asynchronous. Instead, you want to run it asynchronously to the I/O.

In other words, it's not the Calculate method that's asynchronous. It's the UI that wants this to run asynchronously to itself. Remove all that Task.Run clutter from there, it doesn't belong.

What to do next? That depends on your use case. Basically, there's two scenarios:

You want the tasks to always run, always in the background, from start to end. In that case, simply create a thread for each of them, and don't use Task at all. You might also want to explore some options like a producer-consumer queue etc., to optimize the actual run-time of the different possible calculation tasks. The actual implementation is quite tightly bound to what you're actually processing.

Or, you want to start the task on an UI action, and then work with the resulting values back in the UI method that started them when the results are ready. In that case, await finally comes to play:

private btn_Click(object sender, EventArgs e)
{
  var result = await Task.Run(Calculate);

  // Do some (little) work with the result once we get it
  tbxResult.Text = result;
}

The async keyword actually has no place in your code at all.

Hope this is more clear now, feel free to ask more questions.

like image 147
Luaan Avatar answered Mar 23 '23 03:03

Luaan


So what you actually seek is a clarification of a good practice to maximize performance while keeping the UI responsive. As Luaan clarified, the async and await sections in your proposal will not benefit your problem, and Task.Run is not suited for your work; using threads is a better approach.

Define an array of Threads to run one on each logical processor. Distribute your task data between them and control your 20 repetitive calculations via BufferBlock provided in TPL DataFlow library.

To keep UI responsive, I suggest two approaches:

  1. Your calculations demand many frequent UI updates: Put their required update information in a queue and update them in Timer event.
  2. Your calculations demand scarce UI updates: Update UI with an invocation method like Control.BeginInvoke
like image 22
Klaus Avatar answered Mar 23 '23 03:03

Klaus