ConstructorInfo.GetParameters线程安全吗?

时间:2016-03-13 22:14:38

标签: c# .net task-parallel-library xunit.net xunit2

这是一个非常奇怪的问题,我花了一天时间试图追踪。我不确定这是不是一个错误,但是对于为什么会发生这种情况有一些观点和想法会很棒。

我正在使用xUnit(2.0)来运行我的单元测试。 xUnit的优点在于它可以自动为您并行运行测试。但是,我发现的问题是Constructor.GetParametersConstructorInfo被标记为线程安全类型时似乎不是线程安全的。也就是说,如果两个线程同时到达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

所以,问题大多已经解决了。我要感谢所有参与处理我在这件事上的无知,并帮助我学习(我发现的是)这个非常复杂的问题空间。

2 个答案:

答案 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,因此维护该类型的相同实例。