简体   繁体   English

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

[英]Deadlock using Convert.ChangeType within Parallel.ForEach

I just had to debug a code with a deadlock, but I can't really find the reason why. 我只需要调试带有死锁的代码,但是我真的找不到原因。 In detail the deadlock happens when calling Convert.ChangeType within a Parallel.ForEach loop. 详细地说,在Parallel.ForEach循环中调用Convert.ChangeType时会发生死锁。

I try to find any Information about the thread-safety of this method, but I wasn't able to find some. 我尝试找到有关此方法的线程安全性的任何信息,但找不到。 So I had a look at the .NET source code and tried to do what they do, so I don't need to call Convert.ChangeType . 因此,我看了一下.NET源代码,并尝试执行它们的操作,因此不需要调用Convert.ChangeType And finally the code run without the deadlock. 最后,代码运行无死锁。

In my example code I convert an enumeration type to its underlaying ulong type: 在示例代码中,我将枚举类型转换为其底层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
}

To reproduce fe: 要复制fe:

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

But I don't really understand, why Convert.ChangeType is causing a deadlock - does anyone have any idea? 但是我真的不明白,为什么Convert.ChangeType导致死锁-有人知道吗?

Edit : It works with Convert.ChangeType , if I initialize Instance within a static constructor - but why the heck? 编辑 :它与Convert.ChangeType ,如果我在静态构造函数中初始化Instance ,那么为什么要这么做呢?

    public static readonly TestClass<T> Instance = null;

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

The cause is nothing to do with Convert.ChangeType , it just happens to exhibit the issue because the call references the static uLongType field, which causes the TestClass<T> type initializer to run. 原因与Convert.ChangeType无关,它恰好表现uLongType ,因为调用引用了静态uLongType字段,这导致TestClass<T>类型初始化程序运行。

The real culprit is the static Instance field that creates a new TestClass<T> instance. 真正的罪魁祸首是创建新TestClass<T>实例的静态Instance字段。 This creates a potential deadlock as the type initializer requires the instance constructor to complete, but the instance constructor is waiting on multiple threads which in turn wait for the type initializer to complete. 这会导致潜在的死锁,因为类型初始化程序需要实例构造函数完成,但是实例构造函数正在多个线程上等待,而这些线程又会等待类型初始化程序完成。

Adding a static constructor, which removes the beforefieldinit type attribute and changes type initialization behavior as mentioned in the comments, only semi-reliably hides the deadlock in combination with an attached debugger in my tests. 添加一个静态构造函数,该构造函数将删除beforefieldinit类型属性并更改注释中提到的类型初始化行为,只能半可靠地将死锁与附加的调试器结合起来隐藏在我的测试中。 It doesn't really solve the issue. 它并不能真正解决问题。

Here is a simplified example which exhibits the problem most of the time: 这是一个简化的示例,大多数情况下都会出现此问题:

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() { }
}

The deadlocking probability varies depending on the combination of instance and/or static usage in the lambda, attached debugger, release optimizations, static constructor, Console.WriteLine call in the lambda, and random Parallel thread scheduling, but it can always happen. 死锁概率取决于lambda中的实例和/或静态用法,附加的调试器,发布优化,lambda中的静态构造函数, Console.WriteLine调用以及随机Parallel线程调度的组合,但这种情况总是可能发生。

I think the problem is purely that you are performing a blocking operating inside a type initializer. 我认为问题纯粹是在类型初始化程序中执行阻塞操作。 The CLR has to run type initializers inside a lock, as it has to prevent them from ever being run twice, and it uses the same lock for all types. CLR必须在锁内运行类型初始化器,因为它必须防止它们两次运行,并且对所有类型都使用相同的锁。 If you do threading inside your type initializer, and you block, then you risk deadlock. 如果在类型初始值设定项中进行线程化阻塞,则可能会出现死锁。

I think that's exactly what's happening here: 我认为这正是这里发生的事情:

  1. The main thread grabs the type initializer lock and runs the type initializer 主线程抓住类型初始值设定项锁并运行类型初始值设定项
  2. Another thread is spawned, which accesses the Convert class, which needs to run its type initializer. 产生另一个线程,该线程访问Convert类,该类需要运行其类型初始化程序。 So it tries to grab the type initializer lock 因此它尝试获取类型初始值设定项锁
  3. The main thread blocks waiting for the second thread to complete, holding the type initializer lock 主线程阻塞等待第二个线程完成,并保持类型初始化器的锁
  4. Deadlock 僵局

You didn't see this when calling IConvertable.ToUInt64 directly, because that didn't need to call the Convert class's type initializer. 直接调用IConvertable.ToUInt64时,您没有看到此内容,因为不需要调用Convert类的类型初始值设定项。

When your TestClass<T>.Instance is assigned inline, the BeforeFieldInit flag gets set. 当您的TestClass<T>.Instance被内联分配时, BeforeFieldInit标志被设置。 This means the CLR uses a relaxed approach to running the type initializer, and in my testing it ran it before Main , before the type initializer for Convert had been run. 这意味着CLR使用轻松的方法来运行类型初始化器,并且在我的测试中,它在运行Convert的类型初始化器之前在Main之前运行了它。 When you defined an explicit static constructor, the CLR was forced to run the type initializer when TestClass<T>.Instance was first referenced in Main , presumably after Convert had been initialized, which happened to avoid the deadlock. 当您定义一个显式的静态构造函数时,在Main中首先引用TestClass<T>.Instance时,CLR被迫运行类型初始化程序,大概是在Convert初始化之后,这样做避免了死锁。

My evidence for this is knowledge of how type initializers are run, the fact that the thread blocks somewhere inside the runtime (but before it gets a chance to run the method Convert.ChangeType ), and the fact that merely referencing the Convert type is enough to trigger this. 我的证据是了解类型初始化程序的运行方式,线程在运行时内部某处阻塞的事实(但在它有机会运行方法Convert.ChangeType ),以及仅引用Convert类型就足够了触发这个。

See this MSDN article . 请参阅此MSDN文章 I think the takeaway is that you probably shouldn't be doing threading in your type initializer, and you definitely shouldn't be blocking the thread which is running the type initializer. 我认为可以得出的结论是,您可能不应该在类型初始值设定项中进行线程化,并且绝对不应该阻止正在运行类型初始值设定项的线程。

I'd be happy to take at your actual (non-simplified) problem, and try a suggest ways of improving its performance without resorting to threading in the type initializer. 我很乐意为您解决实际的(非简化)问题,并尝试提出一些建议的方法来提高其性能,而无需借助类型初始化器中的线程。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM