以下代码运行时没有问题:
// 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
不是线程安全的,但根据文档说它是线程安全的。
这种行为的解释是什么?
答案 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的普遍存在,但它肯定会改变行为(并且会消除匿名方法作为死锁的来源......当然,人们总是可以重现调用显式声明的静态命名方法的问题。