2

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?

Doug
  • 6,322
  • 3
  • 29
  • 48
  • It might be related to this: http://stackoverflow.com/questions/15143931/strange-behaviour-of-console-readkey-with-multithreading - you are calling Console.WriteLine() in a background thread before it's been initialised properly. – Matthew Watson Nov 18 '15 at 20:32
  • 1
    You got lucky with `sb.AppendLine`, the method is not thread safe, if you had more and longer strings your strings would be malformed or some would even be missing. – Scott Chamberlain Nov 18 '15 at 20:42
  • For me it locks even without `Console.WriteLine` inside `Program`'s static constructor. Calling `ForAll` (with an empty body) in the cctor triggers the hang. (Targeting 4.5.2, VS 2013) – xxbbcc Nov 18 '15 at 20:47
  • I'm just guessing here but I think that `Program`'s static constructor is not finished when the parallel loop is called and the underlying code goes into a waiting state for the static constructor to finish iniitalizing the type (from where the threads start). If you move the logic to a separate static function, it works without hanging. – xxbbcc Nov 18 '15 at 21:00
  • Possible duplicate of this: "http://stackoverflow.com/questions/5770478/plinq-statement-gets-deadlocked-inside-static-constructor" Console.WriteLine has nothing to do with it. – johnnyjob Nov 18 '15 at 22:06

1 Answers1

1

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

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • 1
    Thanks, this explains it very well. It's still nice to point out that the blocking in static method calls occur only between different threads. Static methods called from within the same thread the static constructor is running are called just fine. Maybe not a good practice, tho. – Doug Nov 19 '15 at 13:55
  • _" the blocking in static method calls occur only between different threads"_ -- yes, that is correct. _"Maybe not a good practice"_ -- no, I think it's fine, as long as one is not violating some other good practice wrt static constructors. I.e. assuming the initialization is suitable for a static constructor but wordy enough that factoring it into one or more methods improves the code, then doing that is a _good_ thing, not something to be avoided. I'll edit the question to clarify the point about the cross-thread call. – Peter Duniho Nov 19 '15 at 17:03