1

As I understand it, all three static methods in Parallel (For, ForEach and Invoke) create tasks in the background.

You can stop creating these tasks with cancel a token inside ParallelOptions.

I made two simple examples.

In the first uses the For method in the second Invoke.

In the case of the For method, the behavior is expected, after canceling the token, the creation of new tasks is stopped. In the case of the Invoke method, this does not happen. No matter how many methods I put in the Invoke method, they are always executed after the token is canceled. And I don't understand why this is happening.

On learn.microsoft.com in Parallel.Invoke method It's been said:

The cancellation token passed in with the ParallelOptions structure enables the caller to cancel the entire operation.

Question: Why in the case of the Invoke method, all tasks are executed and canceling the token does nothing? Or maybe I'm doing something wrong, then tell me what.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class MyClass
    {
        public int a = 0;
        public void Add()
        {
            lock (this)
            {
                Thread.Sleep(100);
                Console.WriteLine($"Do Add {DateTime.Now}, a={a}");
                a++;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            void MyMethodForCancel(CancellationTokenSource cancellationTokenSource)
            {
                Random random = new Random();

                while (true)
                {
                    if (random.Next(1, 100) == 50)
                    {
                        cancellationTokenSource.Cancel();
                        Console.WriteLine($"Cancel token {DateTime.Now}");
                        return;
                    }
                }
            }

            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            ParallelOptions parallelOptions = new ParallelOptions();
            parallelOptions.CancellationToken = cancellationTokenSource.Token;

            MyClass myClass2 = new MyClass();
            Action[] actions = new Action[50];

            for (int i = 0; i < actions.Length; i++)
            {
                actions[i] = myClass2.Add;
            }

            Task MyTask1 = Task.Run(() => Parallel.Invoke(parallelOptions, actions));
            Task MyTask2 = Task.Run(() => { Thread.Sleep(1); MyMethodForCancel(cancellationTokenSource); });

            try
            {
                Task.WaitAll(MyTask1, MyTask2);
            }
            catch
            {

            }

            Console.WriteLine($"a = {myClass2.a}"); //a = 50. Always.               
        }
    }
}

1 Answers1

2

Here is my attempt to confirm your claim, that the Parallel.Invoke method ignores the CancellationToken passed to its options. I am creating a CancellationTokenSource that is canceled with a timer after 700 msec, and 20 Actions that each requires 500 msec to complete. Let's pass these 20 actions to Parallel.Invoke, and see what happens:

static class Program
{
    static void Main()
    {
        var cts = new CancellationTokenSource(700);
        cts.Token.Register(() => Print($"The Token was canceled."));
        var options = new ParallelOptions()
        {
            MaxDegreeOfParallelism = 2,
            CancellationToken = cts.Token
        };
        Print("Before starting the Parallel.Invoke.");
        try
        {
            Parallel.Invoke(options, Enumerable.Range(1, 20).Select(i => new Action(() =>
            {
                Print($"Running action #{i}");
                Thread.Sleep(500);
            })).ToArray());
        }
        catch (OperationCanceledException)
        {
            Print("The Parallel.Invoke was canceled.");
        }
    }

    static void Print(object value)
    {
        Console.WriteLine($@"{DateTime.Now:HH:mm:ss.fff} [{Thread.CurrentThread
            .ManagedThreadId}] > {value}");
    }
}

Output:

12:12:46.422 [1] > Before starting the Parallel.Invoke.
12:12:46.450 [1] > Running action #1
12:12:46.451 [5] > Running action #2
12:12:46.951 [1] > Running action #3
12:12:46.951 [5] > Running action #4
12:12:47.122 [7] > The Token was canceled.
12:12:47.458 [1] > The Parallel.Invoke was canceled.

Try it on Fiddle.

It seems that the CancellationToken is respected. Only 4 of the 20 actions are executed, and no more actions are invoked after the cancellation of the token.

Notice however that I have configured the Parallel.Invoke with a small MaxDegreeOfParallelism. This is important. If instead I configure this option with a large value, like 100, or leave it to its default value which is -1 (unbounded), than all 20 actions will be invoked. What happens in this case is that the ThreadPool is saturated, meaning that all the available ThreadPool threads are borrowed aggressively by the Parallel.Invoke method, and there is no available thread to do other things, like invoking the scheduled cancellation of the CancellationTokenSource! So the cancellation is postponed until the completion of the Parallel.Invoke, which is obviously too late.

The moral lesson is: always configure the MaxDegreeOfParallelism option whenever you use the Parallel class. Don't believe the documentation that says: "Generally, you do not need to modify this setting". This is a horrible advice. You should only follow this advice if your intention is to starve your ThreadPool to death.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thank you. 1) I'm testing this in a 2019 development environment under .net 5.0 and this code throws a compilation error. All 4 namespaces are connected. 2) Yes, I experimented with this property and also noticed that this is the case, but why does everything work differently with For and ForEach, and in Invoke, if you set the value not 20, but 500, then 500 tasks will be completed. Are you saying that 500 tasks are created even on a dual-core machine? Most likely, it is not. I think the cancel mechanism in Invoke is not working properly, it should work in both For and ForEach methods. –  Jan 18 '22 at 23:27
  • "all the available ThreadPool threads are borrowed aggressively by the Parallel.Invoke method, and there is no available thread to do other things, like invoking the scheduled cancellation of the CancellationTokenSource" - my example proves that this is not the case, cancellation occurs early enough, but tasks continue to be created and executed. In my example, you can make 1000 array elements and the entire 1000 tasks will be completed, although almost from the very beginning the token was canceled. –  Jan 18 '22 at 23:41
  • @NikVladi I fixed the code so that it compiles on Visual Studio 2019. Regarding the differences between `Invoke`/`For`/`ForEach` you can check this question: [Are Parallel.Invoke and Parallel.ForEach essentially the same thing?](https://stackoverflow.com/questions/10867332/are-parallel-invoke-and-parallel-foreach-essentially-the-same-thing) Also to be honest I haven't run the code in your question, because it's not clear what's the expected and what's the actual behavior of this code. – Theodor Zoulias Jan 18 '22 at 23:54
  • *"although almost from the very beginning the token was canceled."* -- How do you know that? The `Task.Run` requires a thread from the `ThreadPool` to do its work. A saturated `ThreadPool` cannot provide this thread. – Theodor Zoulias Jan 19 '22 at 00:11
  • My code prints to the console a message that the Cancel was called and the date and time, after that operations occur with a later time, which means that the cancellation occurred before all these operations. But all the same, new tasks continue to be created, more and more. 100, 1000, 10000. Or am I misunderstanding something? –  Jan 19 '22 at 00:33
  • 1
    Thanks I have used your code. But I set the property to -1 and made 200 the maximum value. On several runs, I got The Token was canceled message in the middle of the console, and after that, dozens and dozens of new messages. This means that even after canceling the token, tasks continue to be created. –  Jan 19 '22 at 00:40
  • 1
    @NikVladi yeap, I am seeing it too. It seems that setting the parallelism to unbounded disables the cancellation functionality of the `Parallel.Invoke`. It might be a bug. You could consider posting a bug report to the dotnet/runtime GitHub repository. Actually now I am seeing that [you have already done so](https://github.com/dotnet/runtime/issues/63973). :-) – Theodor Zoulias Jan 19 '22 at 00:50
  • 1
    Yes thank you. Let's wait for the answer on github.com/dotnet/runtime/issues. –  Jan 19 '22 at 00:56
  • 1
    Out of curiosity, I created projects under the 4.7.2 framework for the x64 target platform. Your solution and mine work correctly as expected. –  Jan 19 '22 at 04:41