这是一个非常奇怪的问题,我花了一天时间试图追踪。我不确定这是不是一个错误,但是对于为什么会发生这种情况有一些观点和想法会很棒。
我正在使用xUnit(2.0)来运行我的单元测试。 xUnit的优点在于它可以自动为您并行运行测试。但是,我发现的问题是Constructor.GetParameters
在ConstructorInfo
被标记为线程安全类型时似乎不是线程安全的。也就是说,如果两个线程同时到达Constructor.GetParameters
,则会生成两个结果,并且对此方法的后续调用将返回创建的第二个结果(无论调用它的线程如何)。
我已经创建了一些代码来演示这种意外行为(I also have it hosted on GitHub,如果您想在本地下载并试用该项目)。
以下是代码:
public class OneClass
{
readonly ITestOutputHelper output;
public OneClass( ITestOutputHelper output )
{
this.output = output;
}
[Fact]
public void OutputHashCode()
{
Support.Add( typeof(SampleObject).GetTypeInfo() );
output.WriteLine( "Initialized:" );
Support.Output( output );
Support.Add( typeof(SampleObject).GetTypeInfo() );
output.WriteLine( "After Initialized:" );
Support.Output( output );
}
}
public class AnotherClass
{
readonly ITestOutputHelper output;
public AnotherClass( ITestOutputHelper output )
{
this.output = output;
}
[Fact]
public void OutputHashCode()
{
Support.Add( typeof(SampleObject).GetTypeInfo() );
output.WriteLine( "Initialized:" );
Support.Output( output );
Support.Add( typeof(SampleObject).GetTypeInfo() );
output.WriteLine( "After Initialized:" );
Support.Output( output );
}
}
public static class Support
{
readonly static ICollection<int> Numbers = new List<int>();
public static void Add( TypeInfo info )
{
var code = info.DeclaredConstructors.Single().GetParameters().Single().GetHashCode();
Numbers.Add( code );
}
public static void Output( ITestOutputHelper output )
{
foreach ( var number in Numbers.ToArray() )
{
output.WriteLine( number.ToString() );
}
}
}
public class SampleObject
{
public SampleObject( object parameter ) {}
}
这两个测试类确保创建并运行两个线程。运行这些测试后,您应该得到如下所示的结果:
Initialized:
39053774 <---- Different!
45653674
After Initialized:
39053774 <---- Different!
45653674
45653674
45653674
(注意:我添加了“&lt; ---- Different!”来表示意外的值。你不会在测试结果中看到这一点。)
如您所见,第一次调用GetParameters
的结果返回的值与后续调用的值不同。
我已经在.NET中使用了很长一段时间,但从未见过这样的东西。这是预期的行为吗?是否有一种首选/已知的初始化.NET类型系统的方法,以便不会发生这种情况?
最后,如果有人感兴趣,我在使用xUnit和MEF 2 where a ParameterInfo being used as a key in a dictionary is not returning as equal to the ParameterInfo being passed in from a previously saved value时遇到了这个问题。当然,这会导致意外行为,并导致同时运行时测试失败。
编辑:在得到答案的一些好反馈后,我(希望)澄清了这个问题和场景。该问题的核心是“Thead-Safe”类型的“线程安全”,并且更好地了解这意味着什么。答案:这个问题最终归因于几个因素,其中一个原因是由于我对多线程场景永无止境的愚蠢,这似乎是我永远在学习无止境在可预见的未来。我再次感谢xUnit的设计,以这种有效的方式学习这片领土。
另一个问题似乎与.NET类型系统初始化的方式不一致。使用TypeInfo / Type,无论哪个线程多次访问它,您都会获得相同的类型/引用/哈希码。对于MemberInfo / MethodInfo / ParameterInfo,情况并非如此。线程访问要小心。
最后,似乎我不是唯一一个有这种困惑的人,这有indeed been recognized as an invalid assumption on a submitted issue to .NET Core's GitHub repository。
所以,问题大多已经解决了。我要感谢所有参与处理我在这件事上的无知,并帮助我学习(我发现的是)这个非常复杂的问题空间。
答案 0 :(得分:6)
它是第一个调用的一个实例,然后是每个后续调用的另一个实例。
好的,没关系。有点奇怪,但是没有记录该方法,因为每次都会返回相同的实例。
因此,一个线程将在第一次调用时获得一个版本,然后每个线程将获得另一个版本(在每个后续调用中都是不变的实例。
再次,奇怪,但完全合法。
这是预期的行为吗?
好吧,在你的实验之前,我没想到它。但是在你的实验之后,是的,我希望这种行为继续下去。
是否有一种首选/已知的初始化.NET类型系统的方法,以便不会发生这种情况?
据我所知。
如果我使用第一个电话来存储密钥,那么是的,这是一个问题。
然后你有证据表明你应该停止这样做。如果你这样做会伤害,不要这样做。
ParameterInfo引用应始终表示相同的ParameterInfo引用,无论它处于何种线程或访问了多少次。
这是关于如何设计 功能的道德声明。它不是 设计的方式,显然不是它的实现方式。你当然可以提出设计不好的论点。
先生。 Lippert也是正确的,文档不保证/指定这一点,但这一直是我对这种行为的期望和经验,直到这一点。
过往表现并不能保证未来的结果;直到现在你的经历还没有变化。多线程有一种混淆人们期望的方式!一个记忆不断变化的世界,除非某些东西保持不变,这与我们正常的事物模式相反,直到某些东西改变它们为止。
答案 1 :(得分:1)
作为一个答案,我正在研究.NET源代码,而ConstructorInfo类则有这样的内容:
private ParameterInfo[] m_parameters = null; // Created lazily when GetParameters() is called.
那是他们的评论,不是我的评论。我们来看看GetParameters:
[System.Security.SecuritySafeCritical] // auto-generated
internal override ParameterInfo[] GetParametersNoCopy()
{
if (m_parameters == null)
m_parameters = RuntimeParameterInfo.GetParameters(this, this, Signature);
return m_parameters;
}
[Pure]
public override ParameterInfo[] GetParameters()
{
ParameterInfo[] parameters = GetParametersNoCopy();
if (parameters.Length == 0)
return parameters;
ParameterInfo[] ret = new ParameterInfo[parameters.Length];
Array.Copy(parameters, ret, parameters.Length);
return ret;
}
所以没有锁定,没有任何阻止m_parameters被赛车线程覆盖的东西。
更新:以下是GetParameters中的相关代码:args[position] = new RuntimeParameterInfo(sig, scope, tkParamDef, position, attr, member);
很明显,在这种情况下,RuntimeParameterInfo只是其构造函数中给出的参数的容器。甚至没有打算得到相同的实例。
这与TypeInfo不同,TypeInfo继承自Type并且还实现了IReflectableType,并且其GetTypeInfo方法只将其自身返回为IReflectableType,因此维护该类型的相同实例。