我只需要调试带有死锁的代码,但我真的找不到原因。详细地说,死锁发生在调用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>();
}发布于 2019-02-15 09:26:36
我认为问题纯粹在于您在类型初始化器中执行阻塞操作。CLR必须在锁内运行类型初始化器,因为它必须防止它们被运行两次,并且它对所有类型都使用相同的锁。如果在类型初始化器中执行线程处理,然后阻塞,则可能会发生死锁。
我认为这正是这里所发生的事情:
Convert类,该类需要运行其类型初始化程序。因此,它试图获取类型初始化程序锁。在直接调用IConvertable.ToUInt64时没有看到这一点,因为这不需要调用Convert类的类型初始化器。
当您的TestClass<T>.Instance被指定为内联时,将设置BeforeFieldInit标志。这意味着CLR使用了一种轻松的方法来运行类型初始化程序,在我的测试中,它在Main之前、Convert的类型初始化程序运行之前运行。在定义显式静态构造函数时,当TestClass<T>.Instance首次在Main中引用时,CLR被迫运行类型初始化器,这可能是在Convert初始化之后,这是为了避免死锁。
这方面的证据是了解类型初始化器是如何运行的,线程在运行时中的某个位置阻塞(但在它有机会运行Convert.ChangeType方法之前),以及仅仅引用Convert类型就足以触发这个事实。
见这篇MSDN文章。我认为需要注意的是,您可能不应该在您的类型初始化器中执行线程,而且您肯定不应该阻塞正在运行类型初始化程序的线程。
我很乐意考虑您实际的(非简化的)问题,并尝试一个改进其性能的建议方法,而不用使用类型初始化器中的线程。
发布于 2019-02-15 10:29:30
原因与Convert.ChangeType无关,它恰好显示了这个问题,因为调用引用了静态uLongType字段,这会导致TestClass<T>类型初始化程序运行。
真正的罪魁祸首是静态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线程调度,但这种情况总是可能发生的。
https://stackoverflow.com/questions/54696040
复制相似问题