Is ConstructorInfo.GetParameters thread safe?

This is a very strange problem that I spent this day trying to track. I'm not sure if this is a mistake, but it would be great to get some perspective and thoughts on why this is happening.

I use xUnit (2.0) to run my unit tests. The beauty of xUnit is that it automatically runs tests in parallel for you. However, the problem that I discovered is that Constructor.GetParameters does not seem to be thread safe when ConstructorInfo marked as a threadlike type. That is, if two threads reach Constructor.GetParameters at the same time, two results are generated, and subsequent calls to this method return the second result that was created (regardless of the thread that calls it).

I created code to demonstrate this unexpected behavior ( I also posted it on GitHub if you want to download and try the project locally).

Here is the code:

 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 ) {} } 

Two test classes ensure that two threads are created and executed in parallel. After running these tests, you should get results that look like this:

 Initialized: 39053774 <---- Different! 45653674 After Initialized: 39053774 <---- Different! 45653674 45653674 45653674 

(NOTE: I added "<---- Different!" To indicate an unexpected value. This will not be visible in the test results.)

As you can see, the result of the very first GetParameters call returns a different value than all subsequent calls.

I have long had a nose in .NET, but I have never seen anything like it. Is this expected behavior? Is there a preferred / known way to initialize a system like .NET to prevent this from happening?

Finally, if anyone is interested, I ran into this problem when using xUnit with MEF 2, where the ParameterInfo parameter used as the key in the dictionary is not returned as equal to ParameterInfo passed from the previously saved value . This, of course, leads to unexpected behavior and leads to unsuccessful tests while running at the same time.

EDIT: After some good feedback with the answers, I (hopefully) clarified this question and scenario. The core of the problem is Thread Safety, such as Thead-Safe, and a better knowledge of exactly what this means.

ANSWER:. This problem ended up being caused by several factors, one of which is related to the fact that I am infinitely unfamiliar with multi-threaded scripts, which, it seems, I will study forever without end in the foreseeable future. Again, I thank xUnit for being in such a way as to explore this territory in this way.

Another problem seems to be inconsistency with the way a .NET type system is initialized. With TypeInfo / Type, you get the same type / reference / hashcode no matter which thread accesses it, how many times. For MemberInfo / MethodInfo / ParameterInfo this is not the case. Prevent file access.

Finally, it seems that I'm not the only person with this confusion, and this is indeed recognized as an unacceptable assumption about the .NET Core GitHub repository issue presented .

So, the problem is solved, basically. I would like to thank everyone who participated in my ignorance in this matter and help me find out (what I find) this is a very difficult problem space.

+5
source share
2 answers

This is one instance on the first call, and then another instance on each subsequent call.

Good, that’s good. A bit strange, but the method is not documented as always returning the same instance every time.

So, one thread will get one version on the first call, and then each thread will get a different one (an invariable instance for each subsequent call.

Again strange, but completely legal.

Is this the expected behavior?

Well, I would not have expected this before your experiment. But after your experiment, yes, I expect this behavior to continue.

Is there a preferred / known way to initialize a system like .NET to prevent this from happening?

As I know.

If I use this first call to store the key, then yes, this is a problem.

Then you have evidence that you should stop doing this. If it hurts when you do it, don't do it.

The ParameterInfo reference must always represent the same value of the ParameterInfo parameter, regardless of which thread it is on or how many times it has accessed it.

This is a moral statement about how this function should have been designed. This is not how it was designed, and clearly not how it was implemented. You can certainly say that the design is bad.

Mr. Lippert is also right, as the documentation does not guarantee / does not define this, but this has always been my expectation and experience with this behavior up to this point.

Past performance does not guarantee future results; so far your experience has not been diverse enough. Multithreading has a way to mix people's expectations! The world in which memory is constantly changing, if something does not hold it, still contradicts our normal mode of things, which remains unchanged until something changes them.

+6
source

As an answer, I consider .NET sources, and the ConstructorInfo class has this in its depths:

 private ParameterInfo[] m_parameters = null; // Created lazily when GetParameters() is called. 

This is their comment, not mine. See 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; } 

So no blocking, nothing that could prevent the m_parameters overriding by racing thread.

Update: here is the corresponding code inside GetParameters: args[position] = new RuntimeParameterInfo(sig, scope, tkParamDef, position, attr, member); It is clear that in this case RuntimeParameterInfo is just a container for the parameters specified in its constructor. There was not even an intention to get the same copy.

This is not like TypeInfo, which inherits from Type, and also implements IReflectableType and which, for its GetTypeInfo method, simply returns itself as IReflectableType, so the same type instance is supported.

+1
source

Source: https://habr.com/ru/post/1244977/


All Articles