'using' Anweisung vs 'endlich versuchen'

81

Ich habe eine Reihe von Eigenschaften, für die ich Lese- / Schreibsperren verwenden werde. Ich kann sie entweder mit einer try finallyoder einer usingKlausel implementieren .

In der try finallywürde ich das Schloss vor dem erwerben tryund in der freigeben finally. In der usingKlausel würde ich eine Klasse erstellen, die die Sperre in ihrem Konstruktor abruft und in ihrer Dispose-Methode freigibt.

Ich verwende Lese- / Schreibsperren an vielen Stellen, daher habe ich nach Wegen gesucht, die prägnanter sind als try finally. Ich bin daran interessiert, einige Ideen zu hören, warum ein Weg möglicherweise nicht empfohlen wird oder warum einer besser ist als der andere.

Methode 1 ( try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

Methode 2:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}
Jeremy
quelle
1
Wie bereits in vielen Antworten erwähnt, ist Methode 2 sehr gut, aber um bei jeder Verwendung der Sperre Müll auf dem Heap zu vermeiden, sollten Sie ReadLock und WriteLock in Strukturen ändern. Obwohl die using-Anweisung die IDisposable-Schnittstelle einer Struktur verwendet, ist C # klug genug, um Boxen zu vermeiden!
Michael Gehling

Antworten:

95

Von MSDN mit Anweisung (C # -Referenz)

Die using-Anweisung stellt sicher, dass Dispose aufgerufen wird, auch wenn beim Aufrufen von Methoden für das Objekt eine Ausnahme auftritt. Sie können das gleiche Ergebnis erzielen, indem Sie das Objekt in einen try-Block einfügen und dann Dispose in einem finally-Block aufrufen. Auf diese Weise wird die using-Anweisung vom Compiler übersetzt. Das frühere Codebeispiel wird zur Kompilierungszeit auf den folgenden Code erweitert (beachten Sie die zusätzlichen geschweiften Klammern, um den begrenzten Bereich für das Objekt zu erstellen):

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Im Grunde ist es der gleiche Code, aber mit einer schönen automatischen Nullprüfung und einem zusätzlichen Bereich für Ihre Variable . In der Dokumentation heißt es außerdem, dass "die korrekte Verwendung des IDisposable-Objekts sichergestellt wird", sodass Sie in Zukunft möglicherweise noch bessere Framework-Unterstützung für unklare Fälle erhalten.

Fahren Sie also mit Option 2 fort.

Es ist auch ein Plus, die Variable in einem Bereich zu haben , der sofort endet, nachdem sie nicht mehr benötigt wird.

Chakrit
quelle
Was ist am besten, um eine Ressource freizugeben, die nicht mithilfe einer Anweisung instanziiert oder wiederverwendet oder als out-Parameter übergeben werden kann? try / catch !!!
Bjan
2
@bjan gut, warum denkst du überhaupt usingüber diesen Fall nach? dafür ist es nicht da using.
Chakrit
1
deshalb erwähne ich auch, try/catchda es die einzige Möglichkeit ist, über try/catch/finallyBlock zu handhaben . hoffte, usingkönnte auch damit umgehen
bjan
Ja try/finally, das ist deine einzige Option in diesem Fall, denke ich. IMO denke ich jedoch, dass es in solchen Fällen immer ein Objekt / einen Code geben sollte, der für die Aufrechterhaltung der Lebensdauer des Objekts verantwortlich ist (wobei Dispose () immer aufgerufen werden sollte). Wenn eine Klasse nur die Instanziierung übernimmt und sich jemand anderes daran erinnern muss Um es zu entsorgen, riecht es dort ein bisschen. Ich bin mir nicht sicher, wie man das auf Sprachebene hinzufügen würde.
Chakrit
Ich denke, der einzige Grund, das "Verwenden" zu vermeiden, besteht darin, das Einwegprodukt vollständig zu vermeiden, da es eine weitere Objektinstanziierung ist, denke ich?
Demetris Leptos
12

Ich bevorzuge definitiv die zweite Methode. Es ist am Verwendungsort prägnanter und weniger fehleranfällig.

Im ersten Fall muss jemand, der den Code bearbeitet, darauf achten, nichts zwischen dem Aufruf zum Erfassen der Sperre (Lesen | Schreiben) und dem Versuch einzufügen.

(Die Verwendung einer Lese- / Schreibsperre für einzelne Eigenschaften wie diese ist jedoch in der Regel übertrieben. Sie werden am besten auf einer viel höheren Ebene angewendet. Eine einfache Sperre reicht hier häufig aus, da die Möglichkeit von Konflikten angesichts der Zeit, in der die Sperre gehalten wird, vermutlich sehr gering ist (und das Erwerben einer Lese- / Schreibsperre ist eine teurere Operation als eine einfache Sperre).

Rob Walker
quelle
Wie wäre es ausfallsicher zu sein? Ich weiß, dass ein Versuch endlich immer den Block finally auslösen wird. Gibt es eine Möglichkeit, wie die Entsorgung nicht aufgerufen wird?
Jeremy
1
Nein, die using-Anweisung ist im Wesentlichen syntaktischer Zucker für das try / finally-Muster.
Blair Conrad
Das Verwendungsmodell stellt sicher, dass das Objekt immer entsorgt wird.
Nathen Silver
Wenn Sie einen Reflektor verwenden, werden Sie sehen, dass der using-Block vom Compiler in ein try ... finally-Konstrukt übersetzt wird, sodass sie auf IL-Ebene äquivalent sind. Das "Verwenden" ist nur syntatischer Zucker.
Rob Walker
^^ Denken Sie daran, es gibt mögliche Szenarien, in denen schließlich nicht aufgerufen wird. Ein Stromausfall zum Beispiel. ;)
Quibblesome
8

Berücksichtigen Sie die Möglichkeit, dass beide Lösungen schlecht sind, weil sie Ausnahmen maskieren .

Ein tryohne ein catchsollte offensichtlich eine schlechte Idee sein; Siehe MSDN, warum die usingAussage ebenfalls gefährlich ist.

Beachten Sie auch, dass Microsoft jetzt ReaderWriterLockSlim anstelle von ReaderWriterLock empfiehlt.

Beachten Sie schließlich, dass die Microsoft-Beispiele zwei Try-Catch-Blöcke verwenden , um diese Probleme zu vermeiden, z

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

Eine einfache, konsistente und saubere Lösung ist ein gutes Ziel. Wenn Sie jedoch nicht einfach verwenden können lock(this){return mydateetc;}, können Sie den Ansatz überdenken. mit mehr Infos bin ich mir sicher, dass Stack Overflow helfen kann ;-)

Steven A. Lowe
quelle
2
Ein Versuch maskiert schließlich nicht unbedingt die Ausnahme. In meinem Beispiel wird die Sperre erworben. Wenn dann eine Ausnahme innerhalb des Bereichs ausgelöst wird, wird die Sperre freigegeben, aber die Ausnahme sprudelt immer noch.
Jeremy
1
@Jeremy: Wenn Ihr finally-Block eine Ausnahme auslöst, wird die in Ihrem try-Block ausgelöste Ausnahme maskiert - das ist, was der msdn-Artikel sagte, das (gleiche) Problem mit der verwendeten Syntax
Steven A. Lowe
3
Die Ausnahme wird nicht maskiert, sondern genauso ersetzt wie Ihre.
Jon Hanna
5

Ich persönlich verwende die C # -Anweisung "using" so oft wie möglich, aber es gibt einige spezifische Dinge, die ich damit mache, um die genannten potenziellen Probleme zu vermeiden. Um zu veranschaulichen:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

Beachten Sie, dass ich die Anweisung "using" in eine Methode und die Verwendung der Objekte in eine andere Methode mit einem Block "try" / "catch" aufteile. Ich kann mehrere "using" -Anweisungen wie diese für verwandte Objekte verschachteln (manchmal gehe ich drei oder vier tief in meinen Produktionscode ein).

In meinen Dispose()Methoden für diese benutzerdefinierten IDisposableKlassen fange ich Ausnahmen (aber KEINE Fehler) ab und protokolliere sie (mit Log4net). Ich bin noch nie auf eine Situation gestoßen, in der eine dieser Ausnahmen möglicherweise meine Verarbeitung beeinträchtigen könnte. Die potenziellen Fehler können sich wie üblich auf dem Aufrufstapel ausbreiten und die Verarbeitung normalerweise mit einer entsprechenden protokollierten Nachricht (Fehler- und Stapelverfolgung) beenden.

Wenn ich irgendwie auf eine Situation stoßen würde, in der eine signifikante Ausnahme auftreten könnte Dispose(), würde ich diese Situation neu gestalten. Ehrlich gesagt bezweifle ich, dass das jemals passieren wird.

Der Umfang und die Bereinigungsvorteile der "Verwendung" machen es zu einer meiner beliebtesten C # -Funktionen. Übrigens arbeite ich in Java, C # und Python als meine Hauptsprachen, wobei viele andere hier und da hinzukommen, und "Verwenden" ist eine meiner beliebtesten Sprachfunktionen, da es ein praktisches, alltägliches Arbeitstier ist .

Rob Williams
quelle
Tipp für die Lesbarkeit des Codes: Richten Sie die using-Anweisungen aus und komprimieren Sie sie, indem Sie bis auf das innere "using" keine geschweiften Klammern verwenden.
Bent Tranberg
4

Ich mag die 3. Option

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

Von Ihren beiden Optionen ist die zweite Option die sauberste und leichter zu verstehende, was vor sich geht.


quelle
Die Anweisung lock funktioniert nicht wie ReaderWriterLock.
Chakrit
@chakrit: Nein, aber wenn Sie nicht wissen, dass es tatsächlich Sperrenkonflikte gibt, sind sie wahrscheinlich leistungsfähiger.
Michael Burr
Stimmt, aber Sie sollten immer zuerst das Smiple Lock wählen. Die Frage des Subbys enthält keine Angaben zu Leistungsproblemen oder Anforderungen, die keine Sperre zulassen. Daher gibt es keinen Grund zu versuchen, schlau zu sein. Einfach abschließen und rocken.
4

"Bündel von Eigenschaften" und Sperren auf der Ebene der Getter und Setter von Eigenschaften sehen falsch aus. Ihre Verriegelung ist viel zu feinkörnig. Bei den meisten typischen Objektverwendungen möchten Sie sicherstellen, dass Sie eine Sperre erworben haben, um gleichzeitig auf mehrere Eigenschaften zuzugreifen . Ihr spezieller Fall mag anders sein, aber ich bezweifle es irgendwie.

Wenn Sie die Sperre erwerben, wenn Sie auf das Objekt anstatt auf die Eigenschaft zugreifen, wird die Menge an Sperrcode, die Sie schreiben müssen, erheblich reduziert.

Hans Passant
quelle
Ja, ich verstehe Ihren Standpunkt auf jeden Fall. Die meisten meiner Eigenschaften sind Bools, Ints, die ich nicht sperren muss, weil sie atomar sein sollten. Es gibt einige Datums- und Zeichenfolgen, für die ich die Sperre festlegen möchte. Da die Minderheit Schlösser benötigen würde, sollte ich sie am besten auf das Grundstück stellen
Jeremy,
4

DRY sagt: zweite Lösung. Die erste Lösung dupliziert die Logik der Verwendung einer Sperre, die zweite nicht.

Tetha
quelle
1

Try / Catch-Blöcke dienen im Allgemeinen der Ausnahmebehandlung, während Blöcke verwendet werden, um sicherzustellen, dass das Objekt entsorgt wird.

Für die Lese- / Schreibsperre ist ein Versuch / Fang möglicherweise am nützlichsten, aber Sie können auch beide verwenden, wie folgt:

using (obj)
{
  try { }
  catch { }
}

Damit können Sie implizit Ihre IDisposable-Schnittstelle aufrufen und die Ausnahmebehandlung präzise gestalten.

Luke
quelle
0

Ich denke, Methode 2 wäre besser.

  • Einfacherer und besser lesbarer Code in Ihren Eigenschaften.
  • Weniger fehleranfällig, da der Sperrcode nicht mehrmals neu geschrieben werden muss.
Nathen Silver
quelle
0

Obwohl ich vielen der obigen Kommentare zustimme, einschließlich der Granularität der Sperre und der Behandlung fragwürdiger Ausnahmen, handelt es sich bei der Frage um einen Ansatz. Lassen Sie mich einen wichtigen Grund nennen, warum ich es vorziehe, das try {} finally-Modell zu verwenden ... Abstraktion.

Ich habe ein Modell, das Ihrem sehr ähnlich ist, mit einer Ausnahme. Ich habe eine Basisschnittstelle ILock definiert und darin eine Methode namens Acquire () bereitgestellt. Die Acquire () -Methode hat das IDisposable-Objekt zurückgegeben. Dies bedeutet, dass das Objekt, mit dem ich mich befasse, vom Typ ILock ist und mit dem ein Sperrbereich erstellt werden kann. Warum ist das wichtig?

Wir beschäftigen uns mit vielen verschiedenen Verriegelungsmechanismen und Verhaltensweisen. Ihr Sperrobjekt hat möglicherweise ein bestimmtes Zeitlimit, das verwendet wird. Ihre Sperrimplementierung kann eine Monitorsperre, eine Lesersperre, eine Schreibsperre oder eine Drehsperre sein. Aus Sicht des Anrufers ist dies jedoch alles irrelevant. Es ist ihnen wichtig, dass der Vertrag zum Sperren der Ressource eingehalten wird und dass die Sperre dies in einer Weise tut, die mit der Implementierung übereinstimmt.

interface ILock {
    IDisposable Acquire();
}

class MonitorLock : ILock {
    IDisposable Acquire() { ... acquire the lock for real ... }
}

Ich mag Ihr Modell, aber ich würde in Betracht ziehen, die Schlossmechanik vor dem Anrufer zu verstecken. FWIW, ich habe den Overhead der Verwendungstechnik im Vergleich zum Try-finally gemessen, und der Overhead für die Zuweisung des Einwegobjekts liegt zwischen 2-3% Leistungsaufwand.

Ajaxx
quelle
0

Ich bin überrascht, dass niemand vorgeschlagen hat, den Try-finally in anonymen Funktionen zu kapseln. Genau wie bei der Instanziierung und Entsorgung von Klassen mit der using-Anweisung bleibt die Sperre an einem Ort. Ich bevorzuge dies selbst nur, weil ich lieber das Wort "endlich" als das Wort "Entsorgen" lesen möchte, wenn ich darüber nachdenke, ein Schloss freizugeben.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                rwlMyLock_m,
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                rwlMyLock_m,
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private static DateTime ReadLockedMethod(
        ReaderWriterLock rwl,
        ReadLockMethod method
    )
    {
        rwl.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }

    private static void WriteLockedMethod(
        ReaderWriterLock rwl,
        WriteLockMethod method
    )
    {
        rwl.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}

quelle
0

SoftwareJedi, ich habe kein Konto, daher kann ich meine Antworten nicht bearbeiten.

In jedem Fall war die vorherige Version nicht wirklich für den allgemeinen Gebrauch geeignet, da für die Lesesperre immer ein Rückgabewert erforderlich war. Dies behebt Folgendes:

class StackOTest
{
    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            DateTime retval = default(DateTime);
            ReadLockedMethod(
                delegate () { retval = dtMyDateTime_m; }
            );
            return retval;
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private void ReadLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}

quelle
0

Im Folgenden werden Erweiterungsmethoden für die ReaderWriterLockSlim-Klasse erstellt, mit denen Sie Folgendes ausführen können:

var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

Hier ist der Code:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}
Clint Chapman
quelle
0

Um die Lösungen vergleichbar zu machen, würden Sie in Ihrem ersten Beispiel auch dort implementieren IDisposable. Dann würden Sie Dispose()vom finallyBlock aus anrufen , anstatt die Sperre direkt aufzuheben.

Dann wären Sie in Bezug auf die Implementierung von "Äpfeln zu Äpfeln" (und MSIL) (MSIL ist für beide Lösungen gleich). Es ist wahrscheinlich immer noch eine gute Idee, sie zu verwenden, usingda der Umfang erweitert wurde und das Framework die ordnungsgemäße Verwendung sicherstellt IDisposable(letzteres ist weniger vorteilhaft, wenn Sie sich IDisposableselbst implementieren ).

Galaxis
quelle
-1

Wie dumm von mir. Es gibt eine Möglichkeit, dies noch einfacher zu machen, indem die gesperrten Methoden Teil jeder Instanz werden (anstelle von statischen wie in meinem vorherigen Beitrag). Jetzt bevorzuge ich das wirklich, weil es nicht nötig ist, "rwlMyLock_m" an eine andere Klasse oder Methode weiterzugeben.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private DateTime ReadLockedMethod(ReadLockMethod method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(WriteLockMethod method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}

quelle