2016-03-13 28 views
5

Das ist ein sehr merkwürdiges Problem, das ich den Tag versucht habe, aufzuspüren. Ich bin mir nicht sicher, ob das ein Fehler ist, aber es wäre großartig, etwas Perspektive und Gedanken darüber zu bekommen, warum das passiert.Ist ConstructorInfo.GetParameters Thread-sicher?

Ich verwende xUnit (2.0), um meine Komponententests auszuführen. Das Schöne an xUnit ist, dass es automatisch Tests für Sie ausführt. Das Problem, das ich gefunden habe, ist jedoch, dass Constructor.GetParameters nicht Thread-sicher scheint, wenn der ConstructorInfo als Thread-Safe-Typ markiert ist. Das heißt, wenn zwei Threads gleichzeitig Constructor.GetParameters erreichen, werden zwei Ergebnisse erzeugt, und nachfolgende Aufrufe dieser Methode geben das zweite Ergebnis zurück, das erstellt wurde (unabhängig vom Thread, der es aufruft).

Ich habe Code erstellt, um dieses unerwartete Verhalten zu demonstrieren (I also have it hosted on GitHub, wenn Sie das Projekt lokal herunterladen und testen möchten). Hier

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

Die beiden Testklassen sorgen dafür, dass zwei Threads erstellt und parallel ausgeführt werden. Auf diese Tests ausgeführt wird, sollten Sie die Ergebnisse erhalten, die wie folgt aussehen:

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

(ANMERKUNG: Ich habe hinzugefügt, um den unerwarteten Wert bezeichnen Sie werden es nicht sehen in „< ---- Different!“. Die Testergebnisse.)

Wie Sie sehen können, gibt das Ergebnis vom allerersten Aufruf an GetParameters einen anderen Wert zurück als alle nachfolgenden Aufrufe.

Ich hatte meine Nase in .NET für eine ganze Weile, aber habe noch nie so etwas gesehen. Ist das erwartetes Verhalten? Gibt es eine bevorzugte/bekannte Möglichkeit, das .NET-Typsystem zu initialisieren, damit dies nicht geschieht?

Schließlich, wenn jemand interessiert ist, stieß ich auf dieses Problem bei der Verwendung von xUnit mit 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. Dies führt natürlich zu unerwartetem Verhalten und führt bei gleichzeitiger Ausführung zu fehlgeschlagenen Tests.

EDIT: Nach einigem guten Feedback aus den Antworten habe ich (hoffentlich) diese Frage und das Szenario geklärt. Der Kern des Problems ist "Thread-Sicherheit" eines "Thead-Safe" -Typs und ein besseres Wissen darüber, was genau das bedeutet.

ANTWORT: Dieses Problem auf mehrere Faktoren zurückzuführen sein endete als, eine davon ist ich wegen nie endende Ignoranz Multi-Threaded-Szenarien, die es mir ohne Ende für die absehbare Zukunft bin für immer zu lernen scheint . Ich bin wieder dankbar dafür, dass xUnit so konzipiert wurde, dass man dieses Territorium so effektiv lernen kann.

Das andere Problem scheint Inkonsistenzen mit, wie das .NET-Typsystem initialisiert wird. Mit der TypeInfo/Type erhalten Sie den gleichen Typ/Referenz/Hashcode, egal auf welchen Thread zugegriffen wird, aber oft. Für MemberInfo/MethodInfo/ParameterInfo ist dies nicht der Fall. Thread-Zugriff Vorsicht.

Schließlich scheint es, ich bin nicht die einzige Person mit dieser Verwirrung und das hat indeed been recognized as an invalid assumption on a submitted issue to .NET Core's GitHub repository.

Also, Problem meist gelöst. Ich möchte allen, die sich mit meiner Unwissenheit in dieser Angelegenheit beschäftigen, meinen Dank und meine Anerkennung aussprechen und mir helfen, diesen sehr komplexen Problemraum zu lernen (was ich finde).

+0

Und das ist ein Problem? Dass Sie zwei verschiedene Instanzen einer Klasse mit denselben Werten haben? –

+1

Korrekt. Es ist eine Instanz beim ersten Aufruf und dann eine weitere Instanz bei jedem folgenden Aufruf. So erhält ein Thread beim ersten Aufruf eine Version, und dann erhält jeder Thread bei jedem weiteren Aufruf eine andere (unveränderliche Instanz). Wenn ich diesen ersten Aufruf zum Speichern eines Schlüssels verwende (wie im obigen Beispiel mit MEF2), dann Ja, das ist ein Problem :) –

Antwort

6

Es ist eine Instanz beim ersten Aufruf und dann eine weitere Instanz bei jedem nachfolgenden Aufruf.

OK, das ist in Ordnung. Ein bisschen seltsam, aber die Methode ist nicht dokumentiert, da sie jedes Mal die gleiche Instanz zurückgibt.

So wird ein Thread eine Version auf dem ersten Anruf erhalten, und dann wird jeder Thread wird eine andere (unveränderliche Instanz auf jedem nachfolgenden Aufruf erhalten.

Auch seltsam, aber völlig legal.

Ist das erwartete Verhalten?

Nun, würde ich es nicht vor Ihrem Experiment erwartet. Aber nach dem exp Ich erwarte, dass dieses Verhalten anhält.

Gibt es eine bevorzugte/bekannte Möglichkeit, das .NET-Typsystem zu initialisieren, damit dies nicht geschieht?

Nicht zu meinem Wissen.

Wenn ich diesen ersten Anruf verwende, um einen Schlüssel zu speichern, dann ja, das ist ein Problem.

Dann haben Sie Beweise dafür, dass Sie damit aufhören sollten. Wenn es schmerzt, wenn du das tust, tu es nicht.

Ein ParameterInfo-Verweis sollte immer den gleichen ParameterInfo-Verweis darstellen, unabhängig davon, auf welchem ​​Thread er sich befindet oder wie oft auf ihn zugegriffen wurde.

, dass eine moralische Aussage geht darum, wie das Feature sollte sind entworfen worden. Es ist nicht wie es wurde entworfen, und es ist eindeutig nicht, wie es implementiert wurde. Sie können sicherlich argumentieren, dass das Design schlecht ist.

Herr Lippert hat auch Recht, dass die Dokumentation dies nicht garantiert/spezifiziert, aber das war immer meine Erwartung und Erfahrung mit diesem Verhalten bis zu diesem Punkt.

Die frühere Performance ist keine Garantie für zukünftige Ergebnisse; Ihre Erfahrung war bisher nicht ausreichend vielfältig. Multithreading kann die Erwartungen der Menschen durcheinander bringen! Eine Welt, in der sich das Gedächtnis ständig ändert, wenn es nicht still gehalten wird, widerspricht unserer normalen Art, Dinge so zu verändern, bis etwas sie verändert.

+0

Zwei Dinge: Erstens ist es nicht das Array, das einen HashCode erzeugt: Es ist eine 'ParameterInfo', von der ich nicht glaube, dass sie mutiert werden kann. Zweitens, und noch wichtiger, denke ich, es ist vernünftig anzunehmen, dass 'Equals()' entweder für zwei 'ParameterInfo'-Objekte, die den gleichen Parameter darstellen, konsequent' true' zurückgibt oder konsequent 'false' zurückgibt. Fehle ich etwas? – StriplingWarrior

+0

Danke @ Eric-Lippert für Ihre Antwort. Bitte beachten Sie, dass dies nicht ich bin, wer dieses per se benutzt, sondern MEF 2 (System.Composition), wie ich oben erwähnt habe und warum ich den Tag damit verbracht habe, dies aufzuspüren. Es ist in der Tat meine (und MEF 2) Erwartung/Verständnis, dass jeder Aufruf an ein System.Reflection -Element die gleiche Referenz für das abgefragte Objekt zurückgibt. Im obigen Beispiel wird diese Erwartung tatsächlich bei jedem Aufruf mit Ausnahme des ersten Aufrufs erfüllt. Zusätzlich zu meinem Wissen über System.Reflection wird System.Composition ein neues Problem in GitHub bekommen. : P –

+0

@ Mike-EEE: Klingt wie MEF2 hat einen Fehler dann! Netter Fund. –

1

Als Antwort, ich bin auf der Suche auf die .NET-Quellen und die ConstructorInfo Klasse hat dies in seinen Eingeweiden:

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

Dies ist ihr Kommentar ist, nicht meine.Lassen Sie sich GetParameters sehen:

[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; 
} 

Also keine Verriegelung, nichts, was die m_parameters außer Kraft gesetzt durch einen Renn Faden verhindern würde.

Update: Hier ist der relevante Code in GetParameters: args[position] = new RuntimeParameterInfo(sig, scope, tkParamDef, position, attr, member); Es ist klar, dass in diesem Fall RuntimeParameterInfo nur ein Container für die Parameter in seinem Konstruktor angegeben ist. Es gab nie die Absicht, dieselbe Instanz zu bekommen.

Das ist anders als TypeInfo, die von Typ erbt und auch IReflectableType implementiert und die sich für ihre GetTypeInfo-Methode selbst als IReflectableType zurückgibt, so dass dieselbe Instanz des Typs beibehalten wird.

+1

Richtig, aber hier wird nicht das Array der Parameter gehackt - auch ich war momentan verwirrt. Es ist der * Inhalt * des Parameterarrays, der gehashed wird. Aber ich wette, dass der Parameter-Info-Generierungscode in ähnlicher Weise eine träge Logik hat, die nicht rassensicher ist. Und es gibt keinen Grund dafür, dass es rassensicher ist; Das Schlimmste, was passiert, ist, dass Sie zwei Instanzen bekommen, die denselben Inhalt haben und einer verwaist ist. –

+0

Unabhängig vom genauen Mechanismus des Verhaltens macht die Reflection-Engine jedoch keine Versprechungen bezüglich der referenziellen Identität der Objekte, die sie zurückgibt, so dass es * * abhängig * von dieser Identität eine schlechte Idee ist. –

+0

Das Problem hier aus meiner (und anscheinend MEF2) Ansicht ist, dass Sie in einem single-threaded-Szenario * von der referenziellen Integrität von Reflexionsobjekten 100% abhängig sein können. Für mich scheint dies ein Versehen im Rahmen zu sein, aber was weiß ich. Deshalb bin ich hier und stelle Fragen, um es herauszufinden. :) –