2016-03-13 3 views
5

Это очень странная проблема, которую я провел день, пытаясь отследить. Я не уверен, что это ошибка, но было бы здорово получить некоторую перспективу и мысли о том, почему это происходит.Is ConstructorInfo.GetParameters Thread-Safe?

Я использую xUnit (2.0) для запуска модульных тестов. Красота xUnit заключается в том, что она автоматически запускает тесты параллельно для вас. Однако проблема, которую я обнаружил, заключается в том, что Constructor.GetParameters, по-видимому, не является потокобезопасным, если ConstructorInfo помечен как поточно-безопасный тип. То есть, если два потока достигают 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 

(ПРИМЕЧАНИЕ: Я добавил, чтобы обозначить неожиданное значение Вы не увидите это в «< ---- 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. Это, конечно, приводит к неожиданному поведению и приводит к неудачным тестам при одновременном запуске.

EDIT: После некоторой хорошей обратной связи с ответами, я (надеюсь) разъяснил этот вопрос и сценарий. Ядром проблемы является «Безопасность потоков» типа «Thead-Safe» и более полное знание того, что именно это означает.

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

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

И, наконец, кажется, что я не единственный человек с этой путаницей, и у этого есть indeed been recognized as an invalid assumption on a submitted issue to .NET Core's GitHub repository.

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

+0

И это проблема? Что у вас есть два разных экземпляра класса с одинаковыми значениями? –

+1

Исправить. Это один экземпляр при первом вызове, а затем другой экземпляр при каждом последующем вызове. Таким образом, один поток получит одну версию при первом вызове, а затем каждый поток получит другой (неизменный экземпляр для каждого последующего вызова. Если я использую этот первый вызов для хранения ключа (как в примере выше с MEF2), тогда да, это проблема. :) –

ответ

6

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

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

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

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

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

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

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

Насколько мне известно.

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

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

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

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

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

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

+0

Две вещи: во-первых, это не массив, который создает HashCode: это параметр ParameterInfo, который, я думаю, не может быть мутирован. Во-вторых, и что более важно, я думаю, что разумно предположить, что 'Equals()' будет либо последовательно возвращать 'true', либо последовательно возвращать' false' для двух объектов ParameterInfo, которые представляют один и тот же параметр. Я что-то упускаю? – StriplingWarrior

+0

Спасибо @ Eric-Lippert за ваш ответ. Обратите внимание, что это не я, кто использует это как таковое, но MEF 2 (System.Composition), как я уже говорил выше, и почему я провел день, отслеживая это. Это действительно мое (и MEF 2) ожидание/понимание того, что каждый вызов любого элемента System.Reflection возвращает ту же ссылку для запрошенного объекта. В приведенном выше примере это ожидание действительно выполняется с каждым вызовом, за исключением первого вызова. В дополнение к моему знанию System.Reflection, System.Composition собирается получить новую проблему в GitHub. : P –

+0

@ Mike-EEE: Похоже, MEF2 имеет ошибку! Хорошая находка. –

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

+1

Правильно, но это не массив параметров, который хэшируется здесь - я тоже был напуган этим. Это * содержимое * массива параметров хэшируется. Но я готов поспорить, что код генерации информации о параметрах аналогично имеет ленивую логику, которая не является доказательством гонки. И нет * причины * для того, чтобы он был гонодейственным; худшее, что происходит, это то, что вы получаете два экземпляра, которые имеют один и тот же контент, а один - осиротевший. –

+0

Независимо от точного механизма поведения, однако, очевидно, что движок Reflection не дает никаких обещаний относительно ссылочной идентичности объектов, которые он поддерживает, поэтому * в зависимости * от этого тождества - плохая идея. –

+0

Проблема здесь из моего (и, по-видимому, MEF2) зрения заключается в том, что в однопоточном сценарии вы * можете * зависеть от ссылочной целостности объектов отражения на 100%. Для меня это похоже на надзор в рамках, но что я знаю. Вот почему я здесь и задаю вопросы, чтобы узнать наверняка. :) –