Kann das „Verwenden“ mit mehr als einer Ressource ein Ressourcenleck verursachen?

106

Mit C # kann ich Folgendes tun (Beispiel aus MSDN):

using (Font font3 = new Font("Arial", 10.0f),
            font4 = new Font("Arial", 10.0f))
{
    // Use font3 and font4.
}

Was passiert bei font4 = new FontWürfen? Soweit ich weiß, wird font3 Ressourcen verlieren und nicht entsorgt werden.

  • Ist das wahr? (font4 wird nicht entsorgt)
  • Bedeutet dies, using(... , ...)dass eine verschachtelte Verwendung insgesamt vermieden werden sollte?
Benjamin Gruenbaum
quelle
7
Es wird nicht undicht Speicher; im schlimmsten Fall wird es immer noch GC'd bekommen.
SLaks
3
Es würde mich nicht wundern, wenn using(... , ...)es trotzdem mit Blöcken verschachtelt wird, aber das weiß ich nicht genau.
Dan J
1
Das ist nicht das, was ich meinte. Selbst wenn Sie überhaupt nicht verwenden using, wird der GC es schließlich sammeln.
SLaks
1
@zneak: Hätte es zu einem einzigen finallyBlock kompiliert , wäre es nicht in den Block eingetreten, bis alle Ressourcen erstellt worden wären.
SLaks
2
@zneak: Da bei der Konvertierung von a usingin a try- finallyder Initialisierungsausdruck außerhalb von ausgewertet wird try. Es ist also eine vernünftige Frage.
Ben Voigt

Antworten:

158

Nein.

Der Compiler generiert finallyfür jede Variable einen eigenen Block.

Die Spezifikation (§8.13) sagt:

Wenn eine Ressourcenerfassung die Form einer lokalen Variablendeklaration hat, können mehrere Ressourcen eines bestimmten Typs erfasst werden. Eine usingErklärung des Formulars

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement 

ist genau gleichbedeutend mit einer Folge von verschachtelten using-Anweisungen:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
quelle
4
Das ist 8.13 in der C # -Spezifikation Version 5.0, übrigens.
Ben Voigt
11
@WeylandYutani: Was fragst du?
SLaks
9
@WeylandYutani: Dies ist eine Frage-und-Antwort-Seite. Wenn Sie eine Frage haben, starten Sie bitte eine neue Frage!
Eric Lippert
5
@ user1306322 warum? Was ist, wenn ich es wirklich wissen will?
Oxymoron
2
@Oxymoron, dann sollten Sie einige Beweise für Ihre Bemühungen vorlegen, bevor Sie die Frage in Form von Recherchen und Vermutungen veröffentlichen. Andernfalls wird Ihnen dasselbe gesagt, Sie verlieren die Aufmerksamkeit und sind ansonsten mit größerem Verlust konfrontiert. Nur ein Ratschlag, der auf persönlichen Erfahrungen basiert.
user1306322
67

UPDATE : Ich habe diese Frage als Grundlage für einen Artikel verwendet, der hier zu finden ist . Weitere Informationen zu diesem Thema finden Sie hier. Danke für die gute Frage!


Obwohl die Antwort von Schabse natürlich richtig ist und die gestellte Frage beantwortet, gibt es eine wichtige Variante Ihrer Frage, die Sie nicht gestellt haben:

Was passiert, wenn font4 = new Font()Würfe ausgeführt werden, nachdem die nicht verwaltete Ressource vom Konstruktor zugewiesen wurde, aber bevor der ctor zurückkehrt und font4die Referenz ausfüllt ?

Lassen Sie mich das etwas klarer machen. Angenommen, wir haben:

public sealed class Foo : IDisposable
{
    private int handle = 0;
    private bool disposed = false;
    public Foo()
    {
        Blah1();
        int x = AllocateResource();
        Blah2();
        this.handle = x;
        Blah3();
    }
    ~Foo()
    {
        Dispose(false);
    }
    public void Dispose() 
    { 
        Dispose(true); 
        GC.SuppressFinalize(this);
    }
    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (this.handle != 0) 
                DeallocateResource(this.handle);
            this.handle = 0;
            this.disposed = true;
        }
    }
}

Jetzt haben wir

using(Foo foo = new Foo())
    Whatever(foo);

Dies ist das gleiche wie

{
    Foo foo = new Foo();
    try
    {
        Whatever(foo);
    }
    finally
    {
        IDisposable d = foo as IDisposable;
        if (d != null) 
            d.Dispose();
    }
}

OK. Angenommen, WhateverWürfe. Dann wird der finallyBlock ausgeführt und die Ressource freigegeben. Kein Problem.

Angenommen, Blah1()Würfe. Dann erfolgt der Wurf, bevor die Ressource zugewiesen wird. Das Objekt wurde zugewiesen, aber der ctor kehrt nie zurück, wird also foonie ausgefüllt. Wir haben das nie eingegeben, tryalso geben wir auch nie das ein finally. Die Objektreferenz wurde verwaist. Schließlich wird der GC dies feststellen und in die Finalizer-Warteschlange stellen. handleist immer noch Null, also macht der Finalizer nichts. Beachten Sie, dass der Finalizer gegenüber einem Objekt, das finalisiert wird und dessen Konstruktor nie abgeschlossen wurde, robust sein muss . Sie müssen so starke Finalizer schreiben. Dies ist ein weiterer Grund, warum Sie das Schreiben von Finalisierern Experten überlassen und nicht versuchen sollten, dies selbst zu tun.

Angenommen, Blah3()Würfe. Der Wurf erfolgt nach der Zuweisung der Ressource. Aber auch hier foowird nie ausgefüllt, wir geben das nie ein finallyund das Objekt wird vom Finalizer-Thread bereinigt. Diesmal ist der Griff ungleich Null und der Finalizer bereinigt ihn. Wieder läuft der Finalizer auf einem Objekt, dessen Konstruktor nie erfolgreich war, aber der Finalizer wird trotzdem ausgeführt. Offensichtlich muss es, denn diesmal hatte es Arbeit zu erledigen.

Nehmen wir nun an, Blah2()wirft. Der Wurf erfolgt, nachdem die Ressource zugewiesen, aber vorher handle ausgefüllt wurde! Wieder läuft der Finalizer, aber jetzt handleist er immer noch Null und wir lecken den Griff!

Sie müssen äußerst cleveren Code schreiben , um dieses Leck zu verhindern. FontWen interessiert zum Teufel Ihre Ressource? Wir lecken einen Font-Handle, große Sache. Wenn Sie jedoch unbedingt verlangen, dass jede nicht verwaltete Ressource bereinigt wird, unabhängig vom Zeitpunkt der Ausnahmen , haben Sie ein sehr schwieriges Problem.

Die CLR muss dieses Problem mit Sperren lösen. Seit C # 4 wurden Sperren, die die lockAnweisung verwenden, folgendermaßen implementiert:

bool lockEntered = false;
object lockObject = whatever;
try
{
    Monitor.Enter(lockObject, ref lockEntered);
    lock body here
}
finally
{
    if (lockEntered) Monitor.Exit(lockObject);
}

Enterwurde sehr sorgfältig geschrieben, so dass unabhängig davon, welche Ausnahmen ausgelöst werden , lockEnteredgenau dann auf true gesetzt wird, wenn die Sperre tatsächlich aufgehoben wurde. Wenn Sie ähnliche Anforderungen haben, müssen Sie tatsächlich schreiben:

    public Foo()
    {
        Blah1();
        AllocateResource(ref handle);
        Blah2();
        Blah3();
    }

und schreibe AllocateResourcegeschickt Monitor.Enterso, dass unabhängig davon, was im Inneren passiert AllocateResource, das genau dannhandle ausgefüllt wird, wenn es freigegeben werden muss.

Die Beschreibung der Techniken hierfür würde den Rahmen dieser Antwort sprengen. Wenden Sie sich an einen Experten, wenn Sie diese Anforderung haben.

Eric Lippert
quelle
6
@gnat: Die akzeptierte Antwort. Das S muss für etwas stehen. :-)
Eric Lippert
12
@ Joe: Natürlich ist das Beispiel ersonnen . Ich habe es gerade erfunden . Die Risiken sind nicht übertrieben, da ich nicht angegeben habe, wie hoch das Risiko ist. Vielmehr habe ich festgestellt, dass dieses Muster möglich ist . Die Tatsache, dass Sie glauben, dass das Einstellen des Feldes das Problem direkt löst, zeigt genau meinen Punkt: Wie die große Mehrheit der Programmierer, die keine Erfahrung mit dieser Art von Problem haben, sind Sie nicht in der Lage, dieses Problem zu lösen. in der Tat, die meisten Menschen erkennen nicht einmal , dass es ist ein Problem, das ist , warum ich diese Antwort an erster Stelle geschrieben .
Eric Lippert
5
@Chris: Angenommen, zwischen der Zuweisung und der Rückgabe sowie zwischen der Rückgabe und der Zuweisung wird keine Arbeit geleistet. Wir löschen alle diese BlahMethodenaufrufe. Was verhindert, dass an einem dieser Punkte eine ThreadAbortException auftritt?
Eric Lippert
5
@ Joe: Dies ist keine Diskussionsgesellschaft; Ich möchte keine Punkte erzielen, indem ich überzeugender bin . Wenn Sie skeptisch sind und nicht mein Wort dafür nehmen möchten, dass dies ein heikles Problem ist, für dessen korrekte Lösung die Konsultation von Experten erforderlich ist, können Sie mir gerne widersprechen.
Eric Lippert
7
@GilesRoberts: Wie löst das das Problem? Angenommen, die Ausnahme tritt nach dem Aufruf von, AllocateResourceaber vor der Zuweisung an auf x. An ThreadAbortExceptiondiesem Punkt kann A passieren. Jedem hier scheint mein Punkt zu fehlen, nämlich die Erstellung einer Ressource und die Zuordnung eines Verweises darauf zu einer Variablen ist keine atomare Operation . Um das von mir identifizierte Problem zu lösen, müssen Sie es zu einer atomaren Operation machen.
Eric Lippert
32

Als Ergänzung zur Antwort von @SLaks finden Sie hier die IL für Ihren Code:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 74 (0x4a)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] class [System.Drawing]System.Drawing.Font font3,
        [1] class [System.Drawing]System.Drawing.Font font4,
        [2] bool CS$4$0000
    )

    IL_0000: nop
    IL_0001: ldstr "Arial"
    IL_0006: ldc.r4 10
    IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
    IL_0010: stloc.0
    .try
    {
        IL_0011: ldstr "Arial"
        IL_0016: ldc.r4 10
        IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
        IL_0020: stloc.1
        .try
        {
            IL_0021: nop
            IL_0022: nop
            IL_0023: leave.s IL_0035
        } // end .try
        finally
        {
            IL_0025: ldloc.1
            IL_0026: ldnull
            IL_0027: ceq
            IL_0029: stloc.2
            IL_002a: ldloc.2
            IL_002b: brtrue.s IL_0034

            IL_002d: ldloc.1
            IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
            IL_0033: nop

            IL_0034: endfinally
        } // end handler

        IL_0035: nop
        IL_0036: leave.s IL_0048
    } // end .try
    finally
    {
        IL_0038: ldloc.0
        IL_0039: ldnull
        IL_003a: ceq
        IL_003c: stloc.2
        IL_003d: ldloc.2
        IL_003e: brtrue.s IL_0047

        IL_0040: ldloc.0
        IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_0046: nop

        IL_0047: endfinally
    } // end handler

    IL_0048: nop
    IL_0049: ret
} // end of method Program::Main

Beachten Sie die verschachtelten try / finally-Blöcke.

David Heffernan
quelle
17

Dieser Code (basierend auf dem Originalbeispiel):

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (Font font3 = new Font("Arial", 10.0f),
                    font4 = new Font("Arial", 10.0f))
        {
            // Use font3 and font4.
        }
    }
}

Es wird die folgende CIL erstellt (in Visual Studio 2013 für .NET 4.5.1):

.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
    // Code size       82 (0x52)
    .maxstack  2
    .locals init ([0] class [System.Drawing]System.Drawing.Font font3,
                  [1] class [System.Drawing]System.Drawing.Font font4,
                  [2] bool CS$4$0000)
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  nop
    IL_0008:  ldstr      "Arial"
    IL_000d:  ldc.r4     10.
    IL_0012:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                  float32)
    IL_0017:  stloc.0
    .try
    {
        IL_0018:  ldstr      "Arial"
        IL_001d:  ldc.r4     10.
        IL_0022:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                      float32)
        IL_0027:  stloc.1
        .try
        {
            IL_0028:  nop
            IL_0029:  nop
            IL_002a:  leave.s    IL_003c
        }  // end .try
        finally
        {
            IL_002c:  ldloc.1
            IL_002d:  ldnull
            IL_002e:  ceq
            IL_0030:  stloc.2
            IL_0031:  ldloc.2
            IL_0032:  brtrue.s   IL_003b
            IL_0034:  ldloc.1
            IL_0035:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
            IL_003a:  nop
            IL_003b:  endfinally
        }  // end handler
        IL_003c:  nop
        IL_003d:  leave.s    IL_004f
    }  // end .try
    finally
    {
        IL_003f:  ldloc.0
        IL_0040:  ldnull
        IL_0041:  ceq
        IL_0043:  stloc.2
        IL_0044:  ldloc.2
        IL_0045:  brtrue.s   IL_004e
        IL_0047:  ldloc.0
        IL_0048:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
        IL_004d:  nop
        IL_004e:  endfinally
    }  // end handler
    IL_004f:  nop
    IL_0050:  nop
    IL_0051:  ret
} // end of method Class1::.ctor

Wie Sie sehen, try {}startet der Block erst nach der ersten Zuordnung, die um stattfindet IL_0012. Auf den ersten Blick diese nicht erscheinen das erste Element in ungeschützten Code zuzuweisen. Beachten Sie jedoch, dass das Ergebnis an Position 0 gespeichert ist. Wenn die zweite Zuordnung dann fehlschlägt, wird der äußere finally {} Block ausgeführt, und dies holt das Objekt von Position 0, dh der ersten Zuordnung von font3, und ruft seine Dispose()Methode auf.

Interessanterweise ergibt das Dekompilieren dieser Assembly mit dotPeek die folgende rekonstituierte Quelle:

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (new Font("Arial", 10f))
        {
            using (new Font("Arial", 10f))
                ;
        }
    }
}

Der dekompilierte Code bestätigt, dass alles korrekt ist und dass der Code im usingWesentlichen zu verschachtelten usings erweitert ist. Der CIL-Code ist etwas verwirrend anzusehen, und ich musste ihn ein paar Minuten lang anstarren, bevor ich richtig verstanden habe, was passiert ist. Daher wundert es mich nicht, dass einige Geschichten über „alte Frauen“ aufgetaucht sind Dies. Der generierte Code ist jedoch die unangreifbare Wahrheit.

Tim Long
quelle
@Peter Mortensen Ihre Bearbeitung entfernt Teile des IL-Codes (zwischen IL_0012 und IL_0017), was die Erklärung sowohl ungültig als auch verwirrend macht. Dieser Code sollte eine wörtliche Kopie der Ergebnisse sein, die ich erhalten habe, und die Bearbeitung macht dies ungültig. Können Sie bitte Ihre Bearbeitung überprüfen und bestätigen, dass dies Ihre Absicht ist?
Tim Long
7

Hier ist ein Beispielcode, um die Antwort von @SLaks zu beweisen:

void Main()
{
    try
    {
        using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
        {
        }
    }
    catch(Exception ex)
    {
        Console.WriteLine("catch");
    }
    finally
    {
        Console.WriteLine("done");
    }

    /* outputs

        Construct: t1
        Construct: t2
        Dispose: t1
        catch
        done

    */
}

public class TestUsing : IDisposable
{
    public string Name {get; set;}

    public TestUsing(string name)
    {
        Name = name;

        Console.WriteLine("Construct: " + Name);

        if (Name == "t2") throw new Exception();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose: " + Name);
    }
}
wdosanjos
quelle
1
Das beweist es nicht. Wo ist Entsorgen: t2? :)
Piotr Perak
1
Die Frage betrifft die Entsorgung der ersten Ressource in der Verwendungsliste, nicht der zweiten. "Was passiert, wenn font4 = new FontWürfe ausgeführt werden? Soweit ich weiß, werden in font3 Ressourcen verloren gehen und nicht entsorgt."
wdosanjos