0

I'm trying to learn how to use async tasks and am really struggling just to get a simple example working. I provide a very simplified version of what I'm trying to achieve below.

public void Calculate()
{
    int a1 = 1;
    int a2 = 2;
    int b1 = 3;
    int b2 = 4;
    int c3 = 5;
    int c4 = 6;
    int c5 = 7;
    int c6 = 8;
    int c7 = 9;

    // Do next 2 lines in parallel
    int rank1 = Evaluate(a1, a2, c3, c4, c5, c6, c7);
    int rank2 = Evaluate(b1, b2, c3, c4, c5, c6, c7);

    // wait for above 2 lines and use values to get result
    int result = EvaluateOutcome(rank1, rank2);
}

public static int Evaluate(int i1, int i2, int i3, int i4, int i5, int i6, int i7)
{
    // do something complicated and return value - simulate here with random number
    Random rand = new Random();
    return rand.Next(0,10);
}

public static int EvaluateOutcome(int rank1, int rank2)
{
    return rank1 * rank2;
}

Calculating rank1 and rank2 is a long process in my real code but the calculations are independent of each other. I'd like to try to run both calculations at once to hopefully half the processing time. I need to await both calculations completing prior to calculating result.

I think I should be able to do something like:

private async Task Evaluate(int a1, int a2, int b1, int b2, int c3, int c4, int c5, int c6, int c7)
{
    Task task1 = Evaluate(a1, a2, c3, c4, c5, c6, c7);
    Task task2 = Evaluate(b1, b2, c3, c4, c5, c6, c7);

    await Task.WhenAll(task1, task2);
}

But this does not compile and I'm unsure how to get the results from the Task and put them into my rank1 and rank2 int variables. I think this should be incredibly simple but I can't find a clear example to follow. Some help would be appreciated.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Steve W
  • 1,108
  • 3
  • 13
  • 35
  • use `Task.Run()` ... Add `static Task EvaluateAsync(int i1, ...) => Task.Run((i1, ..). => Evaluate(i1,...);` then `Task task1 = EvaluateAsync(a1, ...);` and then after `await Task.WhenAll` you can get `task1.Result` as `rank1` similar stuff with `task2` – Selvin Aug 25 '21 at 14:38
  • That sounds good Selvin. I'd really appreciate if you could put the full code in an answer. Many thanks – Steve W Aug 25 '21 at 14:41
  • Or better: a list of parameters sets, and pass it to `Parallel.ForEach` – Charlieface Aug 25 '21 at 16:59
  • could you show me how please charlie – Steve W Aug 25 '21 at 17:06
  • @Selvin it would be great if you could help with the detail of the code you suggested. I think thats the answer but I'm unable to figure it out myself TIA – Steve W Aug 25 '21 at 18:24

1 Answers1

0

You can use the Task.Run method to offload each calculation to a ThreadPool thread, and the Task.WaitAll method in order to block the current thread until the two calculations are complete:

Task<int> task1 = Task.Run(() => Evaluate(a1, a2, c3, c4, c5, c6, c7));
Task<int> task2 = Task.Run(() => Evaluate(b1, b2, c3, c4, c5, c6, c7));

Task.WaitAll(task1, task2);

int rank1 = task1.Result;
int rank2 = task2.Result;

A more efficient alternative is to use the Parallel.Invoke method. The core difference is that the current thread will participate in the work too, by doing one of the two calculations. So only one additional thread (from the ThreadPool) will be used:

int rank1 = default;
int rank2 = default;

var parallelOptions = new ParallelOptions()
{
    MaxDegreeOfParallelism = Environment.ProcessorCount
};

Parallel.Invoke(parallelOptions,
    () => rank1 = Evaluate(a1, a2, c3, c4, c5, c6, c7),
    () => rank2 = Evaluate(b1, b2, c3, c4, c5, c6, c7)
);

The documentation of the Parallel.Invoke is a bit confusing, by stating that it executes each of the provided actions, possibly in parallel. Don't worry. The only case that the calculations will not happen in parallel is if the ThreadPool is saturated. In which case the first approach (Task.Run+Task.WaitAll) will fail to parallelize the calculations too, while contributing even more to the saturation of the pool. If you want to take the ThreadPool out of the equation, you could pass a custom TaskScheduler with the ParallelOptions, that executes the tasks on a dedicated thread per task, like the one found here. Or use the TaskCreationOptions.LongRunning flag.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thanks Theodor. With the second approach int result = EvaluateOutcome(rank1, rank2); fails to compile when placed after the Parallell Invoke due to unassigned rank1,rank2. Is it OK just to initialise each to zero where you declared them? – Steve W Aug 25 '21 at 21:43
  • Strangely, both approaches makes my code run about 15x slower. Any thoughts why that might be? – Steve W Aug 25 '21 at 21:59
  • @SteveW yeap, the variables must be initialized for the `Parallel.Invoke` version to compile. It is safe to initialize them in this example, but it certainly makes the code less robust. Future modifications could introduce bugs more easily. I don't know how it can be robustified in this aspect honestly. – Theodor Zoulias Aug 25 '21 at 22:38
  • @SteveW as for the parallel version being slower, it's probably because the overall work is too lightweight. Check [this](https://stackoverflow.com/questions/6036120/parallel-foreach-slower-than-foreach "Parallel.ForEach Slower than ForEach") out for more details. – Theodor Zoulias Aug 25 '21 at 22:43