使用Parallel.ForEach中的Convert.ChangeType进行死锁

时间:2019-02-14 17:31:19

标签: c# multithreading

我只需要调试带有死锁的代码,但是我真的找不到原因。详细地说,在Convert.ChangeType循环中调用Parallel.ForEach时会发生死锁。

我尝试找到有关此方法的线程安全性的任何信息,但找不到。因此,我看了一下.NET源代码,并尝试执行它们的操作,因此,我不需要调用Convert.ChangeType。最后,代码运行无死锁。

在我的示例代码中,我将枚举类型转换为其底层ulong类型:

public class TestClass<T> where T : struct, IConvertible
{
    private static readonly Type uLongType = typeof(ulong);
    public static readonly TestClass<T> Instance = new TestClass<T>();

    private readonly Dictionary<string, object> _NumericValues = new Dictionary<string, object>();
    private readonly Dictionary<string, T> _Values = new Dictionary<string, T>();

    public TestClass()
    {
        if (!typeof(T).IsEnum) throw new InvalidOperationException("Enumeration type required");
        Type t = typeof(T);
        foreach (T value in Enum.GetValues(t)) _Values[Enum.GetName(t, value)] = value;
        // Deadlock at Convert.ChangeType
        Parallel.ForEach(ValueNames, new Action<string>((key) =>
        {
            object value = Convert.ChangeType(_Values[key], uLongType);
            lock (_NumericValues) _NumericValues[key] = value;
            // In real life here comes a lot more code...
        }));
        // Works!
        Parallel.ForEach(ValueNames, new Action<string>((key) =>
        {
            object value = ((IConvertible)_Values[key]).ToUInt64(null);
            lock (_NumericValues) _NumericValues[key] = value;
        }));
    }

    public string[] ValueNames => new List<string>(_Values.Keys).ToArray();
}

public enum TestEnum : ulong
{
    Value1,
    Value2,
    Value3
}

要复制f.e。:

System.Diagnostics.Debug.WriteLine(TestClass<TestEnum>.Instance.ValueNames.Length);

但是我不太明白,为什么Convert.ChangeType造成僵局-有人知道吗?

编辑:如果我在静态构造函数中初始化Convert.ChangeType,那么它可以与Instance一起使用-但是为什么要这么做呢?

    public static readonly TestClass<T> Instance = null;

    static TestClass()
    {
        Instance = new TestClass<T>();
    }

2 个答案:

答案 0 :(得分:3)

原因与Convert.ChangeType无关,它恰好表现出问题,因为该调用引用了静态uLongType字段,这导致TestClass<T>类型的初始化程序运行。 / p>

真正的罪魁祸首是创建新Instance实例的静态TestClass<T>字段。这会导致潜在的死锁,因为类型初始化程序需要实例构造函数完成,但是实例构造函数正在多个线程上等待,而这些线程又会等待类型初始化程序完成。

添加一个静态构造函数,该结构将删除beforefieldinit类型属性并更改注释中提到的类型初始化行为,在我的测试中,它只能半可靠地将死锁与附加的调试器结合起来。它并不能真正解决问题。

这是一个简化的示例,大多数情况下都会出现此问题:

static void Main()
{
    new TestClass();
    Console.WriteLine("Not deadlocked");
}

public class TestClass
{
    static Type uLongType = typeof(ulong);
    static TestClass Instance = new TestClass();

    static TestClass() { }

    public TestClass()
    {
        var values = Enumerable.Range(0, 20).ToList();

        Parallel.ForEach(values, (value) =>
        {
            uLongType.ToString();

            //Forcing the lambda to be compiled as an instance method
            //changes the behavior but deadlocks can happen either way
            InstanceMethod();
        });
    }

    void InstanceMethod() { }
}

根据lambda中实例和/或静态用法,附加的调试器,发布优化,静态构造函数,lambda中的Console.WriteLine调用以及随机Parallel线程调度的组合,死锁概率会有所不同,但总会发生。

答案 1 :(得分:1)

我认为问题纯粹是您正在类型初始化程序中执行阻塞操作。 CLR必须在锁内运行类型初始化器,因为它必须防止它们两次运行,并且对所有类型都使用相同的锁。如果您在类型初始值设定项中执行线程操作,则将阻塞,则可能会出现死锁。

我认为这正是这里发生的事情

  1. 主线程抓住类型初始值设定项锁并运行类型初始值设定项
  2. 生成另一个线程,该线程访问Convert类,该类需要运行其类型初始化程序。因此,它尝试获取类型初始值设定项锁定
  3. 主线程阻塞等待第二个线程完成,并保持类型初始化器锁
  4. 僵局

直接调用IConvertable.ToUInt64时没有看到此内容,因为不需要调用Convert类的类型初始值设定项。

TestClass<T>.Instance被内联分配时,BeforeFieldInit标志被置位。这意味着CLR使用轻松的方法来运行类型初始化器,并且在我的测试中,它在Main之前,Convert的类型初始化器运行之前就运行了它。当您定义一个显式静态构造函数时,CLR被迫在TestClass<T>.Instance中首次引用Main时运行类型初始化器,大概是在Convert初始化之后,这样做是为了避免死锁

为此,我的证据是了解如何运行类型初始值设定项,线程在运行时内部某处阻塞的事实(但在它有机会运行方法Convert.ChangeType之前),以及仅引用Convert类型足以触发此操作。

请参见this MSDN article。我认为,要点是,您可能不应该在类型初始值设定项中进行线程化,并且绝对不应该阻止正在运行类型初始值设定项的线程。

我很乐意回答您的实际(非简化)问题,并尝试提出一些建议的方法来提高其性能,而无需借助类型初始化器中的线程。