2012-04-09 4 views
5

Ich arbeite an einem System, das umfangreiche C-API-Interop erfordert. Ein Teil des Interops erfordert eine Initialisierung und ein Herunterfahren des betreffenden Systems vor und nach irgendwelchen Operationen. Wenn dies nicht geschieht, führt dies zu einer Instabilität des Systems. Ich habe dies einfach durch Referenzzählung in einem Kern Einweg-Umgebungsklasse Umsetzung wie folgt aus:Lock-free Referenz Zählen

public FooEnvironment() 
{ 
    lock(EnvironmentLock) 
    { 
    if(_initCount == 0) 
    { 
     Init(); // global startup 
    } 
    _initCount++; 
    } 
} 

private void Dispose(bool disposing) 
{ 
    if(_disposed) 
    return; 

    if(disposing) 
    { 
    lock(EnvironmentLock) 
    { 
     _initCount--; 
     if(_initCount == 0) 
     { 
     Term(); // global termination 
     } 
    } 
    } 
} 

Dies funktioniert gut und erreicht das Ziel. Da jedoch jede Interop-Operation in einem Block mit FooEnvironment-Verwendung verschachtelt werden muss, blockieren wir die gesamte Zeit und das Profiling schlägt vor, dass diese Sperre fast 50% der während der Laufzeit ausgeführten Arbeit ausmacht. Es scheint mir, dass dies ein grundlegendes Konzept ist, das etwas in .NET oder der CLR ansprechen muss. Gibt es eine bessere Möglichkeit, Referenzzählungen durchzuführen?

+5

Sind 'Interlocked.Increment' (und verwandte) was Sie suchen? – harold

+0

Welche Version des .Net-Frameworks verwenden Sie? – pstrjds

+0

Meinen Sie, dass die Initialisierung und das Herunterfahren zwischen * jeder * Operation oder kurz vor der ersten Operation und nach der letzten ausgeführt werden müssen? – jalf

Antwort

5

Dies ist eine schwierigere Aufgabe, als Sie auf den ersten Blick erwarten können. Ich glaube nicht, dass Interlocked.Increment für Ihre Aufgabe ausreicht. Ich erwarte eher, dass Sie etwas Zauberei mit CAS (Compare-and-Swap) durchführen müssen.

Beachten Sie auch, dass es sehr einfach ist, um dieses Recht zu bekommen, aber meistens - Recht ist immer noch völlig falsch, wenn Ihr Programm mit Heisenbugs abstürzt.

Ich empfehle dringend einige echte Forschung, bevor Sie diesen Weg gehen. Ein paar gute Startpunkte springen nach oben, wenn Sie nach "Lock free reference counting" suchen. This Dr. Dobbs article ist nützlich, und this SO Question könnte relevant sein.

Denken Sie vor allem daran, dass sperren frei Programmierung ist schwer. Wenn dies nicht Ihre Spezialität ist, ziehen Sie in Betracht, Ihre Erwartungen hinsichtlich der Granularität Ihrer Referenzzählungen zu überdenken. Es kann viel, viel weniger teuer sein, Ihre grundlegende Refcount-Richtlinie zu überdenken, als einen zuverlässigen Lock-Free-Mechanismus zu erstellen, wenn Sie kein Experte sind. Vor allem, wenn Sie noch nicht wissen, dass eine Lock-Free-Technik tatsächlich schneller sein wird.

+1

+1 die Links sind großartig und der außergewöhnliche Kommentar "Beachten Sie auch, dass es sehr einfach ist, dies größtenteils zu bekommen - richtig, aber meistens - richtig ist immer noch völlig falsch, wenn Ihr Programm mit Heisenbugs abstürzt." Es ist nie gut, wenn Ihr Kunde anruft und sagt: "Manchmal scheint es, wenn ich das Programm starte, stürzt es innerhalb weniger Sekunden ab und dann läuft es manchmal tagelang ohne Probleme" und beim Debuggen findet man eine falsche Lock-Anweisung :) – pstrjds

+0

Vielleicht meine Die Frage hätte allgemeiner sein müssen, da ich auch Vorschläge für leichtere Schließsysteme begrüßen würde. Das grundlegende Problem ist, dass der derzeitige Ansatz sehr teuer ist. Ich hoffe, es wurde nicht impliziert, dass ich nach größtenteils richtigen Antworten suchte, obwohl ich die Beiträge zu schätzen weiß, die zu helfen versuchten. Ich hatte zuvor den Artikel von Dr. Dobbs gelesen, und obwohl ich zugegeben habe, dass es etwas über meinem Kopf ist, hatte ich den Eindruck, dass die vorgestellte Lösung auf bestimmten Prozessoranweisungen beruhte, und dies ist C#. – Jeff

+0

@Jeff haben Sie versucht, Ihr Schloss mit dem 'SpinLock' zu ersetzen, das ist ein billigerer Schließmechanismus, wenn die meisten Sperren schnell sind. – pstrjds

0

Wahrscheinlich eine allgemeine statische Variable in einer Klasse verwenden. Statisch ist nur eine Sache und ist für kein Objekt spezifisch.

+0

EnvironmentLock und _initCount sind beide statisch – Jeff

2

Wie harold's Kommentar stellt fest, ist die Antwort Interlocked:

public FooEnvironment() { 
    if (Interlocked.Increment(ref _initCount) == 1) { 
    Init(); // global startup 
    } 
} 

private void Dispose(bool disposing) { 
    if(_disposed) 
    return; 

    if (disposing) { 
    if (0 == Interlocked.Decrement(ref _initCount)) { 
     Term(); // global termination 
    } 
    } 
} 

Beide Increment und Decrement die neue Zählung (nur für diese Art der Nutzung), also verschiedene Prüfungen zurück.

Aber beachten Sie: dies funktioniert nicht wenn alles andere Gleichzeitigkeitsschutz benötigt. Interlocked Operationen sind selbst sicher, aber nichts anderes ist (einschließlich verschiedener Threads relative Reihenfolge von Interlocked Anrufe). In dem obigen Code Init() kann immer noch ausgeführt werden, nachdem ein anderer Thread den Konstruktor abgeschlossen hat.

+1

"In dem obigen Code kann Init() noch ausgeführt werden, nachdem ein anderer Thread den Konstruktor abgeschlossen hat." << Und deshalb sollte der Lock-Free-Code immer den Experten überlassen werden. :) –

+0

Eigentlich bin ich ok in Init und Term sperren. Dies löst das größere Problem, jedes Mal sperren zu müssen. Vielen Dank! – Jeff

+1

@Jeff: Das löst das Problem nicht. Angenommen, Sie sperren innerhalb von init. Der zweite Thread wird diese Sperre niemals sehen, da er niemals init ausführt. Es wird immer noch die Komponente eingeben, bevor die Initialisierung abgeschlossen ist. –

0

Ich glaube, das wird Ihnen einen sicheren Weg mit Interlocked.Increment/Decrement geben.

Hinweis: Diese stark vereinfacht wird, kann der Code unten zu Deadlock führen wenn Init() eine Ausnahme auslöst. Es gibt auch eine Race-Bedingung in der Dispose, wenn die Anzahl auf Null geht, wird die Init zurückgesetzt und der Konstruktor wird erneut aufgerufen. Ich kenne Ihren Programmablauf nicht, also können Sie besser ein billigeres Schloss wie ein SpinLock im Gegensatz zu dem InterlockedIncrement verwenden, wenn Sie Potential haben, nach mehreren Entsorgungsaufrufen wieder einzuschalten.

static ManualResetEvent _inited = new ManualResetEvent(false); 
public FooEnvironment() 
{ 
    if(Interlocked.Increment(ref _initCount) == 1) 
    { 
     Init(); // global startup 
     _inited.Set(); 
    } 

    _inited.WaitOne(); 
} 

private void Dispose(bool disposing) 
{ 
    if(_disposed) 
     return; 

    if(disposing) 
    { 
     if(Interlocked.Decrement(ref _initCount) == 0) 
     { 
      _inited.Reset(); 
      Term(); // global termination 
     } 
    } 
} 

Edit:
darüber weiter zu denken, Sie können einige Anwendung Redesign zu betrachten und statt dieser Klasse Init und Begriff zu verwalten, haben nur einen einzigen Anruf beim Start der Anwendung Init und einem Rufen Sie Term auf, wenn die App herunterfährt, dann entfernen Sie die Notwendigkeit, ganz zu sperren, und wenn die Sperre zu 50% Ihrer Ausführungszeit angezeigt wird, dann scheint es, als wollten Sie immer Init aufrufen, also rufen Sie einfach an es und weg du gehst.

+0

Beachten Sie, dass Init() ** nicht ** abgeschlossen wird, bevor Thread 2 kommt, inspiziert die Ref-Anzahl und startet (in einer noch nicht initialisierten Umgebung). –

+0

@Greg D - Guter Punkt. Ich wusste, dass es sich anfühlte, als würde mir etwas fehlen. Beweisen Sie einmal mehr, dass Multithreading schwierig ist. – pstrjds

+0

@Greg D - So kann ich es nehmen, Sie werden nicht in die Falle fallen, zu versuchen, eine Lösung zu finden?:) – Jeff

0

Mit dem folgenden Code können Sie fast sperren. Es würde die Konkurrenz definitiv verringern und wenn dies Ihr Hauptproblem ist, wäre es die Lösung, die Sie brauchen.

Auch würde ich empfehlen, Dispose von Destruktor/Finalizer (nur für den Fall) zu nennen. Ich habe Ihre Dispose-Methode geändert - nicht verwaltete Ressourcen sollten ungeachtet des Arguments disposing freigegeben werden. Informationen zur ordnungsgemäßen Entsorgung eines Objekts finden Sie unter this.

Hoffe das hilft dir.

public class FooEnvironment 
{ 
    private static int _initCount; 
    private static bool _initialized; 
    private static object _environmentLock = new object(); 

    private bool _disposed; 

    public FooEnvironment() 
    { 
     Interlocked.Increment(ref _initCount); 

     if (_initCount > 0 && !_initialized) 
     { 
      lock (_environmentLock) 
      { 
       if (_initCount > 0 && !_initialized) 
       { 
        Init(); // global startup 
        _initialized = true; 
       } 
      } 
     } 
    } 

    private void Dispose(bool disposing) 
    { 
     if (_disposed) 
      return; 

     if (disposing) 
     { 
      // Dispose managed resources here 
     } 

     Interlocked.Decrement(ref _initCount); 

     if (_initCount <= 0 && _initialized) 
     { 
      lock (_environmentLock) 
      { 
       if (_initCount <= 0 && _initialized) 
       { 
        Term(); // global termination 
        _initialized = false; 
       } 
      } 
     } 

     _disposed = true; 
    } 

    ~FooEnvironment() 
    { 
     Dispose(false); 
    } 
} 
+0

Danke, leider denke ich, es gibt eine kleine Race-Bedingung zwischen der Überprüfung der _initCount == 0 in der Ctor eines Threads und dispose in einem anderen Thread (so dass wir glauben, dass die Umgebung läuft, wenn es tatsächlich nicht ist). – Jeff

+0

@Jeff, du hast Recht. Ich habe den Code etwas modifiziert. –

0

Threading.Interlocked.Increment Verwendung wird ein wenig schneller als eine Sperre zu erwerben, ein Zuwachs zu tun, und Lösen der Verriegelung, aber nicht enorm so. Der teure Teil beider Operationen auf einem Mehrkernsystem erzwingt die Synchronisation von Speichercaches zwischen Kernen. Der Hauptvorteil von Interlocked.Increment ist nicht Geschwindigkeit, sondern die Tatsache, dass es in einer begrenzten Zeit abgeschlossen wird. Im Gegensatz dazu, wenn jemand versucht, eine Sperre zu erhalten, ein Inkrement auszuführen und die Sperre aufzuheben, besteht auch dann die Gefahr, dass man für einen anderen Thread ewig warten muss, selbst wenn die Sperre zu keinem anderen Zweck als der Überwachung des Zählers verwendet wird erwirbt das Schloss und wird dann weggelegt.

Sie erwähnen nicht, welche Version von .net Sie verwenden, aber es gibt einige Concurrent Klassen, die möglicherweise von Nutzen sind. Je nachdem, wie man Dinge verteilt und freigibt, ist die Klasse ConcurrentBag eine Klasse, die ein wenig schwierig erscheinen mag, aber gut funktionieren könnte. Es ist etwas wie eine Warteschlange oder ein Stapel, außer dass es keine Garantie gibt, dass die Dinge in einer bestimmten Reihenfolge kommen. Fügen Sie in Ihrem Ressourcenwrapper ein Flag ein, das angibt, ob es noch gut ist, und fügen Sie der Ressource selbst einen Verweis auf einen Wrapper hinzu. Wenn ein Ressourcenbenutzer erstellt wird, werfen Sie ein Wrapper-Objekt in den Beutel. Wenn der Ressourcenbenutzer nicht mehr benötigt wird, setzen Sie das Flag "ungültig". Die Ressource sollte am Leben bleiben, solange sich mindestens ein Wrapper-Objekt in der Tasche befindet, dessen "gültiges" Flag gesetzt ist, oder die Ressource selbst einen Verweis auf einen gültigen Wrapper enthält. Wenn ein Element gelöscht wird, scheint die Ressource keinen gültigen Wrapper zu enthalten, eine Sperre zu erhalten und, wenn die Ressource immer noch keinen gültigen Wrapper enthält, Wrapper aus dem Beutel zu ziehen, bis ein gültiger Wrapper gefunden wird Speichere das mit der Ressource (oder, falls keine gefunden wurde, zerstöre die Ressource). Wenn ein Element gelöscht wird, enthält die Ressource einen gültigen Wrapper, aber der Beutel scheint so, als könnte er eine übermäßige Anzahl ungültiger Gegenstände enthalten, die Sperre übernehmen, den Inhalt des Beutels in ein Array kopieren und gültige Gegenstände zurück in den Beutel werfen. Behalte die Anzahl der zurückgeworfenen Gegenstände im Auge, so dass man beurteilen kann, wann die nächste Säuberung durchgeführt wird.

sind über viele Eckfällen sorgen Dieser Ansatz kann komplizierter erscheinen als Schlösser oder Threading.Interlocked.Increment, und es verwenden, aber es kann eine bessere Leistung bieten, weil ConcurrentBag konzipiert Ressourcenkonflikte zu reduzieren.Wenn der Prozessor 1 an irgendeinem Ort Interlocked.Increment ausführt, und dann der Prozessor 2, muss der Prozessor 2 den Prozessor 1 anweisen, diesen Ort aus seinem Cache zu löschen, warten, bis der Prozessor 1 dies getan hat, und alle anderen Prozessoren darüber informieren, dass er die Kontrolle benötigt Laden Sie diesen Ort in seinen Cache und kommen Sie schließlich dazu, ihn zu inkrementieren. Nach all dem ist, wenn Prozessor 1 den Standort erneut inkrementieren muss, dieselbe allgemeine Sequenz von Schritten erforderlich. All dies ist sehr langsam. Die ConcurrentBag-Klasse ist dagegen so konzipiert, dass mehrere Prozessoren ohne Cache-Kollision Dinge zu einer Liste hinzufügen können. Irgendwann, wenn Dinge hinzugefügt und wenn sie entfernt werden, müssen sie in eine kohärente Datenstruktur kopiert werden, aber solche Operationen können in Stapeln so ausgeführt werden, dass eine gute Cache-Leistung erzielt wird.

Ich habe einen Ansatz wie oben unter Verwendung ConcurrentBag nicht versucht, so weiß ich nicht, welche Art von Leistung es tatsächlich ergeben würde, aber je nach Nutzungsmuster kann es möglich sein, bessere Leistung zu geben, als erhalten würde über Referenzzählung.

0

Interlocked-Klasse-Ansatz funktioniert ein wenig schneller als die Sperre-Anweisung, aber auf einer Multi-Core-Maschine der Geschwindigkeitsvorteil möglicherweise nicht sehr, da Interlocked-Anweisungen die Speicher-Cache-Schichten umgehen müssen.

Wie wichtig ist es, die Funktion Term() aufzurufen, wenn der Code nicht verwendet wird und/oder wenn das Programm beendet wird?

Häufig können Sie einfach den Aufruf von Init() einmal in eine static constructor für die Klasse, die die anderen APIs umschließt, und nicht wirklich sorgen, Term() aufzurufen. Z. B:

static FooEnvironment() { 
    Init(); // global startup 
} 

Die CLR stellt sicher, dass der statische Konstruktor wird einmal aufgerufen, vor allen anderen Elementfunktionen in der umgebenden Klasse.

Es ist auch möglich, Benachrichtigungen über einige (aber nicht alle) Anwendungsstilllegungsszenarios zu senden, wodurch es möglich ist, Term() bei Clean Shutdowns aufzurufen. Siehe diesen Artikel. http://www.codeproject.com/Articles/16164/Managed-Application-Shutdown