我只需要调试带有死锁的代码,但是我真的找不到原因。详细地说,在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>();
}
答案 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必须在锁内运行类型初始化器,因为它必须防止它们两次运行,并且对所有类型都使用相同的锁。如果您在类型初始值设定项和中执行线程操作,则将阻塞,则可能会出现死锁。
我认为这正是这里发生的事情
Convert
类,该类需要运行其类型初始化程序。因此,它尝试获取类型初始值设定项锁定直接调用IConvertable.ToUInt64
时没有看到此内容,因为不需要调用Convert
类的类型初始值设定项。
当TestClass<T>.Instance
被内联分配时,BeforeFieldInit
标志被置位。这意味着CLR使用轻松的方法来运行类型初始化器,并且在我的测试中,它在Main
之前,Convert
的类型初始化器运行之前就运行了它。当您定义一个显式静态构造函数时,CLR被迫在TestClass<T>.Instance
中首次引用Main
时运行类型初始化器,大概是在Convert
初始化之后,这样做是为了避免死锁
为此,我的证据是了解如何运行类型初始值设定项,线程在运行时内部某处阻塞的事实(但在它有机会运行方法Convert.ChangeType
之前),以及仅引用Convert
类型足以触发此操作。
请参见this MSDN article。我认为,要点是,您可能不应该在类型初始值设定项中进行线程化,并且绝对不应该阻止正在运行类型初始值设定项的线程。
我很乐意回答您的实际(非简化)问题,并尝试提出一些建议的方法来提高其性能,而无需借助类型初始化器中的线程。