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?
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.