The need for two cancellation tokens in .NET 6 Parallel.ForEachAsync?
Asked Answered
Y

2

4

I was experimenting with how to break out of a ForEachAsync loop. break doesn't work, but I can call Cancel on the CancellationTokenSource. The signature for ForEachAsync has two tokens - one as a stand-alone argument and one in the Func body signature.

I took note that when cts.Cancel() is called, both the token and t variables have IsCancellationRequested set to true. So, my question is: what is the purpose for the two separate token arguments? Is there a distinction worth noting?

List<string> symbols = new() { "A", "B", "C" };
var cts = new CancellationTokenSource();
var token = cts.Token;
token.ThrowIfCancellationRequested();

try
{
    await Parallel.ForEachAsync(symbols, token, async (symbol, t) =>
    {
        if (await someConditionAsync())
        {
            cts.Cancel();
        }
    });
catch (OperationCanceledException oce)
{
    Console.WriteLine($"Stopping parallel loop: {oce}");
}
finally
{
    cts.Dispose();
}
Yardman answered 1/12, 2021 at 21:15 Comment(0)
L
7

Token passed to the body of the method invoked by ForEachAsync is a different one and comes from a internal CancellationTokenSource which will be canceled:

  • on "external" cancelation (that's why you see t.IsCancellationRequested set to true when cts.Cancel() is called)
  • for internal reasons (one that I found - any iteration has thrown an uncaught exception, i.e. fail fast principle is applied).

So the purpose of cancellationToken CancellationToken argument passed to the Parallel.ForEachAsync is to support cancellation by caller and the one passed to the asynchronous delegate invoked by it - to support cancelation both by external (i.e. caller) and internal sources (see the P.S.).

P.S.

Also note that usually it is a good idea to pass and check the token state in your methods (i.e. await someConditionAsync(t) with corresponding implementation inside) since CancelationToken is used for so called cooperative cancelation.

Lading answered 1/12, 2021 at 21:47 Comment(0)
P
-2

Parallel.ForEachAsync takes a token that you can use to cancel the for-each (an input to the function), that token is also passed to each iteration of the for-each (an input to the lambda).

One of the reasons for passing in the cancellation token to the lambda is to avoid the capture of a variable that is outside of the lambda expression.

Imagine this code:

await Parallel.ForEachAsync(symbols, token, async (symbol, t) => MyCode(symbol, t));

Task async MyCode(string symbol, CancellationToken token)
{
    if (await someConditionAsync())
    {
        cts.Cancel();
    }
});

Written this way, MyCode has no access to token.

Using a lambda means you can 'inherit' variables outside the lambda, but that doesn't mean you should.

Pectoral answered 1/12, 2021 at 21:48 Comment(1)
Your MyCode function needs to be async Task, not void - and if you do that then you may-as-well just pass MyCode by-name instead of instantiating an identity function (and it annoys me that C# cannot elide identity lambdas...)Flexor

© 2022 - 2025 — McMap. All rights reserved.