The need for two tokens in .NET 6 Parallel.ForEachAsync?

Viewed 543

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();
}
2 Answers

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:

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.

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.

Related