The following code runs with no issues:
// This code outputs:
// 3
// 2
// 1
//
// foo
// DotNetFiddle: https://dotnetfiddle.net/wDRD9L
public class Program
{
public static void Main()
{
Console.WriteLine("foo");
}
static Program()
{
var sb = new System.Text.StringBuilder();
var list = new List<int>() { 1,2,3 };
list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { sb.AppendLine(item.ToString()); });
Console.WriteLine(sb.ToString());
}
}
As soon as I replace sb.AppendLine
with a call to Console.WriteLine
the code hangs, like there's a deadlock somewhere.
// This code hangs.
// DotNetFiddle: https://dotnetfiddle.net/pbhNR2
public class Program
{
public static void Main()
{
Console.WriteLine("foo");
}
static Program()
{
var list = new List<int>() { 1,2,3 };
list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { Console.WriteLine(item.ToString()); });
}
}
At first I suspected Console.WriteLine
was not thread-safe, but according to documentation it is thread-safe.
What's the explanation for this behavior?
The short version: don't ever block inside a constructor, and especially not in a static
constructor.
In your example, the difference has to do with the anonymous method you use. In the first case, you've captured a local variable which causes the anonymous method to be compiled into its own class. But in the second case, there's no variable capturing and so a static
method suffices. Except that the static method is put into the Program
class. Which is still being initialized.
So, the call to the anonymous method is blocked by the initialization of the class (you can't, from a thread other than where the static constructor is being executed, execute a method in a class until that class has completed initialization), and the initialization of the class is blocked by the execution of the anonymous method (the ForAll()
method won't return until all of those methods have executed).
Deadlock.
It is difficult to know what a good proposal of a work-around might be, given that the example is (as expected) a simplified version of whatever you're really doing. But the bottom line is that that you shouldn't be doing long-running computations in the static constructor. If it's a slow enough algorithm that it justifies the use of ForAll()
, then it's slow enough that it really shouldn't be part of the class initialization in the first place.
Among many possible options for addressing the issue, one you might choose is the Lazy<T>
class, which makes it easy to defer some initialization until it's actually needed.
For example, let's assume that your parallel code is not just writing out elements of the list but is actually processing them in some way. I.e. it's part of the actual initialization of the list. Then you can wrap that initialization in a factory method executed by Lazy<T>
on demand instead of in the static constructor:
public class Program
{
public static void Main()
{
Console.WriteLine("foo");
}
private static readonly Lazy<List<int>> _list = new Lazy<List<int>>(() => InitList());
private static List<int> InitList()
{
var list = new List<int>() { 1,2,3 };
list.AsParallel().WithDegreeOfParallelism(4).ForAll(item => { Console.WriteLine(item.ToString()); });
return list;
}
}
Then the initialization code won't even be executed at all, until some code needs to access the list, which it can do via _list.Value
.
This is subtly different enough that I felt it warranted a new answer (i.e. the kind of use of the anonymous method changes the behavior), but there are at least two other very closely related questions and answers on Stack Overflow:
Plinq statement gets deadlocked inside static constructor
Task.Run in Static Initializer
As an aside: I learned recently that with the new Roslyn compiler, they have changed how they implement anonymous methods in this scenario, and even ones that could be static methods are made instance methods in a separate class (if I recall correctly). I don't know whether this was to reduce the prevalence of this kind of bug or not, but it would definitely change the behavior (and would eliminate the anonymous method as a source of deadlock…of course one could always still reproduce the problem with a call to an explicitly declared static, named method).