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();
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Vic F
  • 1,143
  • 1
  • 11
  • 26

2 Answers2

6

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
-1

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.

Neil
  • 11,059
  • 3
  • 31
  • 56
  • 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...) – Dai Dec 01 '21 at 21:58