1

I have a simple asynchronous code below : this is a WPF with one button and one textBox. I used some list with five integers to mimic 5 different tasks. My intention was to achieve that when I run all five tasks in parallel and asynchronously , I can observe that the numbers are one by one added to the textbox. And I achieved it. Method "DoSomething" run all five tasks in parallel and each of the task has different execution time (simulated by Task.Delay) so all results in the numbers appearing in the textbox one by one. The only problem that I cannot figure out is: why in the textbox I have the string text "This is end text" displayed at first ?! If I await method DoSomething then it should be accomplished first and then the rest of the code should be executed.Even though in my case is a repainting of the GUI.

I guess that this might be caused by the use of Dispacher.BeginInvoke which may "cause some disturbance " to async/await mechanism. But I would appreciate small clue and how to avoid this behawior. I know that I could use the Progress event to achieve similar effect but is there any other way that I can use Parallel loop and update results progressively in WPF avoiding such a unexpected behaviour which I described?

 private async void Button_Click(object sender, RoutedEventArgs e)
    {
        await DoSomething();

        tbResults.Text += "This is end text";
    }


    private async Task DoSomething()
    {
        List<int> numbers = new List<int>(Enumerable.Range(1, 5));

      await Task.Run(()=> Parallel.ForEach(numbers,async  i =>
        {
          await Task.Delay(i * 300);
          await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
        }));
    }
// output is:
//This is end text 1 2 3 4 5 (all in separatę lines).

My questions:

  1. Why the text is displayed prior the method DoSomething .
  2. How to solve it/ avoid it , any alternative way to solve it ( except using Progress event ). Any info will be highly appreciate.
kalka79
  • 11
  • 7
  • 2
    See here: https://stackoverflow.com/q/12337671/1136211 and https://stackoverflow.com/q/11564506/1136211 – Clemens Nov 14 '21 at 15:38

2 Answers2

2

The threads of Parallel.Foreach are "real" background threads. They are created and the application continues execution. The point is that Parallel.Foreach is not awaitable, therefore the execution continues while the threads of the Parallel.Foreach are suspended using await.

private async Task DoSomething()
{
    List<int> numbers = new List<int>(Enumerable.Range(1, 5));

    // Create the threads of Parallel.Foreach
    await Task.Run(() => 
      { 
        // Create the threads of Parallel.Foreach and continue
        Parallel.ForEach(numbers,async  i =>
        {
          // await suspends the thread and forces to return.
          // Because Parallel.ForEach is not awaitable, 
          // execution leaves the scope of the Parallel.Foreach to continue.
          await Task.Delay(i * 300);

          await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
        });

        // After the threads are created the internal await of the Parallel.Foreach suspends background threads and
        // forces to the execution to return from the Parallel.Foreach.
        // The Task.Run thread continues.

        Dispatcher.InvokeAsync(() => tbResults.Text += "Text while Parallel.Foreach threads are suspended");

        // Since the background threads of the Parallel.Foreach are not attached 
        // to the parent Task.Run, the Task.Run completes now and returns
        // i.e. Task.run does not wait for child background threads to complete.
        // ==> Leave Task.Run as there is no work. 
      });

      // Leave DoSomething() and continue to execute the remaining code in the Button_Click(). 
      // Parallel.Foreach threads is still suspended until the await chain, in this case Button_Click(), is completed.
}

The solution is to implement the pattern suggested by Clemens' comment or an async implementation of the Producer Consumer pattern using e.g., BlockingCollection or Channel to gain more control over the fixed number of threads while distributing the "unlimited" number of jobs.

private async Task DoSomething(int number)
{
  await Task.Delay(number * 300);
  Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}

private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
  List<int> numbers = new List<int>(Enumerable.Range(1, 5));
  List<Task> tasks = new List<Task>();

  // Alternatively use LINQ Select
  foreach (int number in numbers)
  {
    Task task = DoSomething(number);
    tasks.Add(task);
  }

  await Task.WhenAll(tasks);

  tbResults.Text += "This is end text" + Environment.NewLine;
}

Discussing the comments

"my intention was to run tasks in parallel and "report" once they are completed i.e. the taks which takes the shortest would "report" first and so on."

This is exactly what is happening in the above solution. The Task with the shortest delay appends text to the TextBox first.

"But implementing your suggested await Task.WhenAll(tasks) causes that we need to wait for all tasks to complete and then report all at once."

To process Task objects in their order of completion, you would replace Task.WhenAll with Task.WhenAny. In case you are not only interested in the first completed Task, you would have to use Task.WhenAny in an iterative manner until all Task instances have been completed:

Process all Task objects in their order of completion

private async Task DoSomething(int number)
{
  await Task.Delay(number * 300);
  Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}

private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
  List<int> numbers = new List<int>(Enumerable.Range(1, 5));
  List<Task> tasks = new List<Task>();

  // Alternatively use LINQ Select
  foreach (int number in numbers)
  {
    Task task = DoSomething(number);
    tasks.Add(task);
  }

  // Until all Tasks have completed
  while (tasks.Any())
  {
    Task<int> nextCompletedTask = await Task.WhenAny(tasks);

    // Remove the completed Task so that
    // we can await the next uncompleted Task that completes first
    tasks.Remove(nextCompletedTask);

    // Get the result of the completed Task
    int taskId = await nextCompletedTask;
    tbResults.Text += $"Task {taskId} has completed." + Environment.NewLine;
  }

  tbResults.Text += "This is end text" + Environment.NewLine;
}

"Parallel.ForEach is not awaitable so I thought that wrapping it up in Task.Run allows me to await it but this is because as you said "Since the background threads of the Parallel.Foreach are not attached to the parent Task.Run""

No that's not exactly what I have said. The key point is the third sentence of my answer: "The point is that Parallel.Foreach is not awaitable, therefore the execution continues while the threads of the Parallel.Foreach are suspended using await.".
This means: normally Parallel.Foreach executes synchronously: the calling context continues execution when all threads of Parallel.Foreach have completed. But since you called await inside those threads, you suspend them in an async/await manner.
Since Parallel.Foreach is not awaitable, it can't handle the await calls and acts like the suspended threads have completed naturally. Parallel.Foreach does not understand that the threads are just suspended by await and will continue later. In other words, the await chain is broken as Parallel.Foreach is not able to return the Task to the parent awaited Task.Run context to signal its suspension.
That's what I meant when saying that the threads of Parallel.Foreach are not attached to the Task.Run. They run in total isolation from the async/await infrastructure.

"async lambdas should be "only use with events""

No, that's not correct. When you pass an async lambda to a void delegate like Action<T> you are correct: the async lambda can't be awaited in this case. But when passing an async lambda to a Func<T> delegate where T is of type Task, your lamda can be awaited:

private void NoAsyncDelegateSupportedMethod(Action lambda)
{
  // Since Action does not return a Task (return type is always void),
  // the async lambda can't be awaited
  lambda.Invoke();
}

private async Task AsyncDelegateSupportedMethod(Func<Task> asyncLambda)
{
  // Since Func returns a Task, the async lambda can be awaited
  await asyncLambda.Invoke();
}

public voi DoSoemthing()
{
  // Not a good idea as NoAsyncDelegateSupportedMethod can't handle async lamdas: it defines a void delegate
  NoAsyncDelegateSupportedMethod(async () => await Task.Delay(1));

  // A good idea as AsyncDelegateSupportedMethod can handle async lamdas: it defines a Func<Task> delegate
  AsyncDelegateSupportedMethod(async () => await Task.Delay(1));
}

As you can see your statement is not correct. You must always check the signature of the called method and its overloads. If it accepts a Func<Task> type delegate you are good to go.
That's how async support is added to Parallel.ForeachAsync: the API supports a Func<ValueTask> type delegate. For example Task.Run accepts a Func<Task> and therefore the following call is perfectly fine:

Task.Run(async () => await Task.Delay(1));

" I guess that you admit that .Net 6.0 brought the best solution : which is Parallel.ForEachASYNC! [...] We can spawn a couple of threads which deal with our tasks in parallel and we can await the whole loop and we do not need to wait for all tasks to complte- they "report" as they finish "

That's wrong. Parallel.ForeachAsync supports threads thatr use async/await, that's true. Indeed, your original example would no longer break the intended flow: because Parallel.ForeachAsync supports await in its threads, it can handle suspended threads and propagate the Task object properly from its threads to the caller context e.g., to the wrapping await Task.Run.
It now knows how to wait for and resume suspended threads.
Important: Parallel.ForeachAsync still completes AFTER ALL its threads have completed. You assumption "they "report" as they finish" is wrong. That's the most intuitive concurrent implementation of a foreach. foreach also completes after all items are enumerated.
The solution to process Task objects as they complete is still using the Task.WhenAny pattern from above.

In general, if you you don't need the extra features like partitioning etc. of the the Parallel.Foreach and Parallel.ForeachAsync, you can always use Task.WhenAll instead. Task.WhenAll and especially Parallel.ForeachAsync are equivalent, except for Parallel.ForeachAsync provides greater customization by default: it suppors techniques like throttling and partitioning without the extra code.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • Wow, I must admit that I was wrong. Indeed when using Task.WhenAll - task with the shortest delay appends text to the TextBox first. Your comment made me realize that there is a still a some way ahead of me of learning async stuff . Your comment was very valuable and describing . Part about "await chain" as well as explanation of async lambda is great. Tank you very much indeed. – kalka79 Nov 18 '21 at 13:36
  • @kalka79 Very nice move to come back to admit your wrongs. Very strong . I'm happy I could help. – BionicCode Nov 18 '21 at 14:34
0

Good links from Clemens see in comment. Answering your questions:

  1. In Parallel.ForEach you start/fire for each entry of numbera an async task, which you don't await. So you do only await, that Parallel.ForEach does finish and it does finish before the async tasks of it.
  2. What you could do e.g. remove async inside of Parallel.ForEach and use Dispatcher.Invoke instead of Dispatcher.BeginInvoke, Thread.Sleep is an anti-pattern ;) , so depending on your task may be take another solution(edited: BionicCode has a nice one):
private async Task DoSomething()
{
   var numbers = new List<int>(Enumerable.Range(1, 5));
   await Task.Run(()=> Parallel.ForEach(numbers, i =>
   {
      Thread.Sleep(i * 300);
      Dispatcher.Invoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
   }));
}
Rekshino
  • 6,954
  • 2
  • 19
  • 44