5

这是一个非常奇怪的问题,我花了一天时间试图追查。我不确定这是否是一个错误,但最好能对为什么会发生这种情况有一些看法和想法。

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

我创建了一些代码来演示这种意外行为(如果您想在本地下载并尝试该项目,我也将它托管在 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返回的值与所有后续调用不同。

我在 .NET 中已经有很长一段时间了,但从未见过像这样的东西。这是预期的行为吗?是否有初始化 .NET 类型系统的首选/已知方法,以免发生这种情况?

最后,如果有人感兴趣,我在使用带有 MEF 2 的 xUnit 时遇到了这个问题,其中用作字典中的键的 ParameterInfo 不返回等于从先前保存的 value 传入的 ParameterInfo。当然,这会导致意外行为并导致同时运行时测试失败。

编辑:从答案中得到一些好的反馈后,我(希望)澄清了这个问题和场景。问题的核心是“Thead-Safe”类型的“Thread-Safety”,并更好地了解这究竟意味着什么。

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

另一个问题似乎与 .NET 类型系统的初始化方式不一致。使用 TypeInfo/Type,无论哪个线程多次访问它,您都会获得相同的类型/引用/哈希码。对于 MemberInfo/MethodInfo/ParameterInfo,情况并非如此。线程访问要小心。

最后,似乎我不是唯一一个有这种困惑的人,这确实被认为是对 .NET Core 的 GitHub 存储库提交的问题的无效假设

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

4

2 回答 2

6

它是第一次调用的一个实例,然后是每个后续调用的另一个实例。

好没问题。有点奇怪,但该方法没有记录为每次都返回相同的实例。

因此,一个线程将在第一次调用时获得一个版本,然后每个线程将获得另一个(在每次后续调用时实例不变。

再次,奇怪,但完全合法。

这是预期的行为吗?

好吧,在你做实验之前我没想到会这样。但是在你的实验之后,是的,我希望这种行为会继续下去。

是否有初始化 .NET 类型系统的首选/已知方法,以免发生这种情况?

据我所知不是。

如果我使用第一次调用来存储密钥,那么是的,这是一个问题。

然后你有证据表明你应该停止这样做。如果这样做时会感到疼痛,请不要这样做。

ParameterInfo 引用应始终表示相同的 ParameterInfo 引用,无论它所在的线程或访问次数如何。

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

Lippert 先生也是对的,因为文档并没有保证/具体说明这一点,但在此之前,这一直是我对这种行为的期望和经验。

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

于 2016-03-13T22:40:42.997 回答
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 返回,因此维护该类型的相同实例。

于 2016-03-13T22:46:47.110 回答