Является ли 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, таким образом поддерживая тот же экземпляр типа.