使用Console和PLinq表观死锁

时间:2015-11-18 20:27:32

标签: c# .net parallel.foreach plinq

以下代码运行时没有问题:

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

只要我通过拨打sb.AppendLine来代替Console.WriteLine,代码就会挂起,就像某处出现死锁一样。

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

起初我怀疑Console.WriteLine不是线程安全的,但根据文档说它是线程安全的。

这种行为的解释是什么?

1 个答案:

答案 0 :(得分:1)

简短版本:不要在构造函数中阻塞,尤其不能在static构造函数中阻塞。

在您的示例中,差异与您使用的匿名方法有关。在第一种情况下,您已捕获了一个局部变量,该变量导致匿名方法被编译到自己的类中。但在第二种情况下,没有变量捕获,因此static方法就足够了。除了将静态方法放入Program类。哪个仍在初始化。

因此,对类的初始化阻止了对匿名方法的调用(你不能从除执行静态构造函数之外的线程中执行,在类中执行一个方法,直到该类具有完成初始化),并通过执行匿名方法阻止类的初始化(ForAll()方法不会返回,直到所有这些方法都已执行)。

死锁。


鉴于该示例(正如预期的那样)是您正在做的任何事情的简化版本,因此很难知道解决方案的优秀提议是什么。但最重要的是,您不应该在静态构造函数中进行长时间运行的计算。如果它是一个足够慢的算法,它证明使用ForAll()是合理的,那么它的速度足够慢,以至于它首先不应该成为类初始化的一部分。

在解决该问题的许多可能选项中,您可能选择的是Lazy<T>类,这样可以很容易地推迟一些初始化,直到实际需要它为止。

例如,让我们假设您的并行代码不只是写出列表的元素,而是实际上以某种方式处理它们。即它是列表实际初始化的一部分。然后,您可以根据需要而不是在静态构造函数中将该初始化包装在由Lazy<T>执行的工厂方法中:

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;
    }
}

然后初始化代码甚至不会被执行,直到某些代码需要访问列表,它可以通过_list.Value来执行。


这有点微妙的不同,我认为它保证了一个新的答案(即使用匿名方法的方式改变了行为),但Stack Overflow上至少有两个非常密切相关的问题和答案:
Plinq statement gets deadlocked inside static constructor
Task.Run in Static Initializer


顺便说一下:我最近了解到,使用新的Roslyn编译器,他们已经改变了在这种情况下实现匿名方法的方式,甚至可能是静态方法的方法也是在单独的类中实例化方法(如果我没记错的话)。我不知道这是否是为了减少这种bug的普遍存在,但它肯定会改变行为(并且会消除匿名方法作为死锁的来源......当然,人们总是可以重现调用显式声明的静态命名方法的问题。