ConstructorInfo.GetParameters 是线程安全的吗?

Is ConstructorInfo.GetParameters Thread-Safe?

这是一个非常棘手的运行ge 问题,我花了一天的时间试图找到它。我不确定这是否是一个错误,但如果能对发生这种情况的原因有一些看法和想法,那就太好了。

我正在使用 xUnit (2.0) 运行 我的单元测试。 xUnit 的美妙之处在于它会自动为您 运行s 进行并行测试。但是,我发现的问题是,当 ConstructorInfo 被标记为线程安全类型时,Constructor.GetParameters 似乎不是线程安全的。也就是说,如果两个线程同时到达Constructor.GetParameters,则产生两个结果,随后对该方法的调用returns产生的第二个结果(不管调用它的线程是什么)。

我已经创建了一些代码来演示这种意外行为(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

(注意:我添加了“<---- 不同!”来表示意外值。您不会在测试结果中看到它。)

如您所见,第一次调用 GetParameters returns 的结果与所有后续调用的值不同。

我接触 .NET 已经有一段时间了,但从未见过这样的东西。这是预期的行为吗?是否有 preferred/known 初始化 .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。这当然会导致意外行为,并导致 运行 同时发生时测试失败。

编辑:在回答了一些很好的反馈之后,我(希望)澄清了这个问题和场景。问题的核心是 "Thread-Safety" 的 "Thead-Safe" 类型,并更好地了解这到底是什么意思。

回答:这个问题最终是由几个因素造成的,其中之一是由于我对多线程场景的无休止忽略运行ce ,在可预见的未来,我似乎永远在学习,没有尽头。我再次感谢 xUnit 以如此有效的方式设计来学习这个领域。

另一个问题确实与 .NET 类型系统的初始化方式不一致。使用 TypeInfo/Type,无论哪个线程访问它多少次,您都会得到相同的 type/reference/hashcode。对于MemberInfo/MethodInfo/ParameterInfo,情况并非如此。线程访问当心。

最后,似乎我不是唯一有这种困惑的人,这有 indeed been recognized as an invalid assumption on a submitted issue to .NET Core's GitHub repository

所以,问题基本上解决了。我想向所有参与处理我的 igno运行ce 并帮助我学习(我发现的是)这个非常复杂的问题的人表示感谢和赞赏 space.

It is one instance on the first call, and then another instance on every subsequent call.

好的,没关系。有点奇怪,但该方法没有记录为每次总是返回相同的实例。

So, one thread will get one version on the first call, and then each thread will get another (unchanging instance on each subsequent call.

同样,很奇怪,但完全合法。

Is this expected behavior?

好吧,在你的实验之前我不会期望它。但是在你的实验之后,是的,我希望这种行为继续下去。

Is there a preferred/known way of initializing the .NET type system so that this does not happen?

我不知道。

If I am using that first call to store a key then yes, that is a problem.

那么你有证据表明你应该停止这样做。如果做的时候疼,就不要做。

A ParameterInfo reference should always represent the same ParameterInfo reference regardless of the thread it is on or how many times accessed.

这是关于应该如何设计功能的道德声明。它不是 的设计方式,显然也不是它的实现方式。你当然可以说设计不好。

Mr. Lippert is also right in that the documentation does not guarantee/specify this, but this has always been my expectation of and experience with this behavior until this point.

过去的表现并不能保证未来的结果;直到现在,你的经历还不够丰富。多线程有一种混淆人们期望的方式!一个记忆不断变化的世界,除非有什么东西使它保持静止,这与我们的正常模式相反,即事物在某些东西改变之前都是一样的。

作为答案,我正在查看 .NET 源代码和 ConstructorInfo class 的内容:

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 不同,它继承自 Type 并且还实现了 IReflectableType,并且它的 GetTypeInfo 方法只是 returns 本身作为一个 IReflectableType,因此维护了相同的类型实例。