使用 Console 和 PLinq 时出现明显的死锁

问题描述 投票:0回答:1

以下代码运行没有问题:

// 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
不是线程安全的,但根据文档它是线程安全的。

此行为的解释是什么?

c# .net parallel.foreach plinq
1个回答
2
投票

简短的版本:永远不要在构造函数内阻塞,尤其是在

static
构造函数中。

在您的示例中,差异与您使用的匿名方法有关。在第一种情况下,您捕获了一个局部变量,该变量导致匿名方法被编译到它自己的类中。但在第二种情况下,没有变量捕获,因此

static
方法就足够了。只是静态方法被放入
Program
类中。仍在初始化中。

因此,对匿名方法的调用会被类的初始化阻止(除了执行静态构造函数的线程之外,您不能在类中执行方法,直到该类完成初始化),并且类的初始化被匿名方法的执行所阻止(在所有这些方法执行完成之前,

ForAll()
方法不会返回)。

死锁。


鉴于该示例(如预期的那样)是您真正正在做的事情的简化版本,因此很难知道解决方法的好建议是什么。但最重要的是,您不应该在静态构造函数中进行长时间运行的计算。如果它是一个足够慢的算法,足以证明使用

ForAll()
是合理的,那么它就足够慢,以至于它一开始就不应该成为类初始化的一部分。

在解决该问题的许多可能选项中,您可以选择

Lazy<T>
类,它可以轻松地将某些初始化推迟到实际需要时为止。

例如,假设您的并行代码不仅写出列表的元素,而且实际上以某种方式处理它们。 IE。它是列表实际初始化的一部分。然后,您可以将该初始化包装在由

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 语句在静态构造函数内陷入僵局
Task.Run 在静态初始化器中


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

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.