0

I'm trying to understand why a shared CancellationTokenSource variable is not protected by a lock or memory barriers here.

I know there is a rule of thumb that a read or a write of a shared (state) variable can be reordered with local variables reads and writes if compiler optimizations are allowed.

Here is an example from the CancellationTokenSource documentation.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        // Define the cancellation token.
        CancellationTokenSource source = new CancellationTokenSource();
        CancellationToken token = source.Token;

        Random rnd = new Random();
        Object lockObj = new Object();

        List<Task<int[]>> tasks = new List<Task<int[]>>();
        TaskFactory factory = new TaskFactory(token);
        for (int taskCtr = 0; taskCtr <= 10; taskCtr++)
        {
            int iteration = taskCtr + 1;
            tasks.Add(factory.StartNew(() =>
            {
                int value;
                int[] values = new int[10];
                for (int ctr = 1; ctr <= 10; ctr++)
                {
                    lock (lockObj)
                    {
                        value = rnd.Next(0, 101);
                    }
                    if (value == 0)
                    {
                        source.Cancel();
                        Console.WriteLine("Cancelling at task {0}", iteration);
                        break;
                    }
                    values[ctr - 1] = value;
                }
                return values;
            }, token));
        }
        try
        {
            Task<double> fTask = factory.ContinueWhenAll(tasks.ToArray(), (results) =>
            {
                Console.WriteLine("Calculating overall mean...");
                long sum = 0;
                int n = 0;
                foreach (var t in results)
                {
                    foreach (var r in t.Result)
                    {
                        sum += r;
                        n++;
                    }
                }
                return sum / (double)n;
            }, token);
            Console.WriteLine("The mean is {0}.", fTask.Result);
        }
        catch (AggregateException ae)
        {
            foreach (Exception e in ae.InnerExceptions)
            {
                if (e is TaskCanceledException)
                    Console.WriteLine("Unable to compute mean: {0}",
                                      ((TaskCanceledException)e).Message);
                else
                    Console.WriteLine("Exception: " + e.GetType().Name);
            }
        }
        finally
        {
            source.Dispose();
        }
    }
}

What's the exact reason for this? Does Microsoft imply that such a reordering would be safe and hence requires no protection measures, or no reordering is possible at all?

bimjhi
  • 311
  • 2
  • 11
  • The `source` is created at the very first line of the program, and never reassigned after that. Could you describe the exact reordering scenario that you are concerned about? – Theodor Zoulias Oct 12 '21 at 17:16
  • 7
    Cancel internally uses interlocked operations to ensure proper ordering. You can see it [in the reference source](https://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs,723). Also, the Cancel cannot be moved ahead of the preceding `lock`. `lock` itself is a barrier. – Raymond Chen Oct 12 '21 at 17:39
  • 1
    @bimjhi AFAIK there is no official documentation available. What information we have comes from blog posts and unofficial comments here and there. You might find these two questions informative: [Memory barrier generators](https://stackoverflow.com/questions/6581848/memory-barrier-generators) and [Are you there, asynchronously written value?](https://stackoverflow.com/questions/28871878/are-you-there-asynchronously-written-value) – Theodor Zoulias Oct 12 '21 at 18:54
  • 3
    If you could reorder code past a lock, what would be the point of `lock` in the first place? [Memory Barrier by lock statement](https://stackoverflow.com/questions/2844452/memory-barrier-by-lock-statement) notes that a `lock` statement erects a full barrier at both entry and exit. – Raymond Chen Oct 12 '21 at 18:59
  • In general, any lock acquisition must erect an acquire barrier at a minimum, and any lock release must erect a release barrier at a minimum. (This is so obvious it usually goes unsaid. If it didn't do these things, the lock would fail at its stated purpose!) – Raymond Chen Oct 12 '21 at 19:05

1 Answers1

1

As it was noted by the author of The Old New Thing in his comment, source.Cancel(); instruction placed in multithreaded code is protected from reordering by means of its internal implementation.

https://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs,723 states that CancellationTokenSource relies upon Interlocked class methods.

According to Joe Albahari, all methods on the Interlocked class in C# implicitly generate full fences: http://www.albahari.com/threading/part4.aspx#_Memory_Barriers_and_Volatility

So one can freely place a call to CancellationTokenSource.Cancel method inside a delegate body without an additional lock or memory barrier if they need to protect it while accessed by multiple tasks.

bimjhi
  • 311
  • 2
  • 11