4

Can somebody explain the behaviour of this code:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello world");
        try
        {
            Parallel.ForEach(new[] { 1, 2, 3 }, async i =>
            {
                await Task.Delay(TimeSpan.FromSeconds(3));
                throw new TaskCanceledException();
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.WriteLine("Goodbye cruel world");
        Console.ReadLine();
    }
}

How can it be possible, the exception pops up in the main thread out of "try" block and application fall. I knew the best way for parallel async is "Task.WhenAll". The goal of the question is to understand the behaviour.

Jonik
  • 1,208
  • 2
  • 12
  • 20

1 Answers1

5

Parallel.ForEach can't be used to call asynchronous methods. It's only meant for in-memory data parallelism, not asynchronous operations. It works by partitioning its data and feeding each batch to a worker task, using roughly one worker per CPU core. It even uses the calling thread to process one of those partitions, which is why it seems to be "blocking".

None of its overloads accepts a Task which means the return type of the lambda in this case is async void.

The question's code is equivalent to :

async void Do(int i)
{
    await Task.Delay(TimeSpan.FromSeconds(3));
    throw new TaskCanceledException();
}

Parallel.ForEach(data,Do);

async void methods can't be awaited which means their exceptions can't be caught by the caller either. This code fires off 3 async void calls using and returns immediately.

In .NET 6 you should use Parallel.ForEachAsync instead :

var data=new[] { 1, 2, 3 };
await Parallel.ForEachAsync(data, async (i,token) =>
{
    await Task.Delay(TimeSpan.FromSeconds(3));
    throw new TaskCanceledException();
});
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thanks for reply! But I still have some misunderstud. The main question for me is why application goes in break mode? I thought, that exception inside delegate should be gone with it's thread, but exceptions pops up to unhandled and breaks the app domain. I expected the behaviour similar to `Task.Run(async ()=> throw new Exception())`, but it's kinda "similar" to `await Task.Run(async ()=> throw new Exception())` – Jonik Jun 03 '22 at 11:48
  • @Jonik you're using the debugger *and* blocking the application from exiting. Without that `Console.ReadLine()` the application would exit immediately. The debugger breaks inside the worker task, not the outer exception. In production there wouldn't be anything to inspect that exception. – Panagiotis Kanavos Jun 03 '22 at 11:50
  • this code is just example, instead of `Console.ReadLine()`, you can use `while (true) { Thread.Sleep(1000); }` and application break too. I want to understand, how/why exception can pops up to unhandled and break the application. Excuse me if I can't explain the question correct, the english isn't native – Jonik Jun 03 '22 at 12:00