Является ли ConstructorInfo.GetParameters поточно-ориентированным?

Это очень странная проблема, которую я провел весь день, пытаясь выследить. Я не уверен, является ли это ошибкой, но было бы здорово получить некоторое представление и соображения о том, почему это происходит.

Я использую 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

(ПРИМЕЧАНИЕ: я добавил "<---- Different!" Для обозначения неожиданного значения. Вы не увидите этого в результатах теста.)

Как видите, результат самого первого звонка GetParameters возвращает значение, отличное от всех последующих вызовов.

У меня уже давно есть нос в.NET, но я никогда не видел ничего подобного. Это ожидаемое поведение? Есть ли предпочтительный / известный способ инициализации системы типов.NET, чтобы этого не произошло?

Наконец, если кому-то интересно, я столкнулся с этой проблемой при использовании xUnit с MEF 2, где ParameterInfo, используемый в качестве ключа в словаре, не возвращается как равный ParameterInfo, передаваемому из ранее сохраненного значения. Это, конечно, приводит к неожиданному поведению и приводит к неудачным тестам при одновременном запуске.

РЕДАКТИРОВАТЬ: После некоторых хороших отзывов от ответов, я (надеюсь) прояснил этот вопрос и сценарий. Суть проблемы - "Потокобезопасность" типа "Thead-Safe", которая позволяет лучше понять, что именно это означает.

ОТВЕТ: Эта проблема, в конечном итоге, возникла из-за нескольких факторов, один из которых связан с тем, что я постоянно борюсь с невежеством в отношении многопоточных сценариев, которые, как мне кажется, я изучаю навсегда без конца в обозримом будущем. Я снова благодарен xUnit за то, что он был разработан таким образом, чтобы изучать эту территорию таким эффективным способом.

Другая проблема заключается в несоответствии с инициализацией системы типов.NET. С TypeInfo/Type вы получаете один и тот же тип /reference/hashcode, независимо от того, какой поток обращается к нему сколько угодно раз. Для MemberInfo/MethodInfo/ParameterInfo это не так. Доступ к потокам остерегается.

Наконец, мне кажется, что я не единственный, кто столкнулся с этой путаницей, и это действительно было признано недопустимым допущением по представленной проблеме в репозиторий.NET Core GitHub.

Итак, проблема решена, в основном. Я хотел бы выразить свою благодарность и признательность всем, кто имеет отношение к моему невежеству в этом вопросе, и помочь мне изучить (что я нахожу в этом) эту очень сложную проблемную область.

2 ответа

Решение

Это один экземпляр при первом вызове, а затем другой экземпляр при каждом последующем вызове.

ОК, хорошо. Немного странно, но метод не задокументирован, так как всегда возвращает один и тот же экземпляр каждый раз.

Таким образом, один поток получит одну версию при первом вызове, а затем каждый поток получит другую (неизменный экземпляр при каждом последующем вызове).

Опять странно, но совершенно законно.

Это ожидаемое поведение?

Ну, я бы не ожидал этого до твоего эксперимента. Но после вашего эксперимента, да, я ожидаю, что такое поведение продолжится.

Есть ли предпочтительный / известный способ инициализации системы типов.NET, чтобы этого не произошло?

Не в моих знаниях.

Если я использую этот первый вызов для хранения ключа, то да, это проблема.

Тогда у вас есть доказательства того, что вы должны прекратить это делать. Если тебе больно, когда ты это делаешь, не делай этого.

Ссылка ParameterInfo всегда должна представлять одну и ту же ссылку ParameterInfo независимо от того, в каком потоке он находится или сколько раз к нему обращались.

Это моральное утверждение о том, как должна была быть разработана эта функция. Дело не в том, как оно было разработано, и явно не в том, как оно было реализовано. Вы, конечно, можете утверждать, что дизайн плохой.

Г-н Липперт также прав в том, что документация не гарантирует и не определяет это, но до этого момента я всегда ожидал такого опыта и имел опыт такого поведения.

Прошлые показатели не являются гарантией будущих результатов; Ваш опыт не был достаточно разнообразным до сих пор. Многопоточность способ сбить людей с толку! Мир, в котором память постоянно меняется, если что-то ее не удерживает, противоречит нашему обычному образу жизни, пока что-то не изменит их.

В качестве ответа я смотрю на исходники.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, который наследуется от Type и также реализует IReflectableType и который для своего метода GetTypeInfo просто возвращает себя как IReflectableType, таким образом поддерживая тот же экземпляр типа.

Другие вопросы по тегам