Può "utilizzare" con più di una risorsa causare una perdita di risorse?


106

C # mi consente di fare quanto segue (esempio da MSDN):

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

Cosa succede se font4 = new Fontlanci? Da quello che ho capito, font3 perderà risorse e non verrà smaltito.

  • È vero? (font4 non verrà smaltito)
  • Questo significa che using(... , ...)dovrebbe essere evitato del tutto a favore dell'uso annidato?

7
Non sarà perdita di memoria; nel peggiore dei casi, otterrà comunque GC.
SLaks

3
Non sarei sorpreso se using(... , ...)fosse compilato in blocchi usando annidati a prescindere, ma non lo so per certo.
Dan J

1
Non è quello che intendevo. Anche se non lo usi usingaffatto, il GC alla fine lo raccoglierà comunque.
SLaks

1
@zneak: se fosse stato compilato in un singolo finallyblocco, non sarebbe entrato nel blocco finché tutte le risorse non fossero state costruite.
SLaks

2
@zneak: poiché nella conversione di a usingin a try- finally, l'espressione di inizializzazione viene valutata al di fuori di try. Quindi è una domanda ragionevole.
Ben Voigt

Risposte:


158

No.

Il compilatore genererà un finallyblocco separato per ogni variabile.

La specifica (§8.13) dice:

Quando un'acquisizione di risorse assume la forma di una dichiarazione di variabile locale, è possibile acquisire più risorse di un dato tipo. Una usingdichiarazione della forma

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

è esattamente equivalente a una sequenza di istruzioni using annidate:

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

4
Questo è 8.13 nella specifica C # versione 5.0, btw.
Ben Voigt

11
@WeylandYutani: Cosa stai chiedendo?
SLaks

9
@WeylandYutani: questo è un sito di domande e risposte. Se hai una domanda, inizia una nuova domanda per favore!
Eric Lippert

5
@ user1306322 perché? E se volessi davvero sapere?
Oxymoron

2
@Oxymoron allora dovresti fornire alcune prove dello sforzo prima di postare la domanda sotto forma di ricerca e ipotesi, altrimenti ti verrà detto lo stesso, perderai attenzione e altrimenti sarai in perdita maggiore. Solo un consiglio basato sull'esperienza personale.
user1306322

67

AGGIORNAMENTO : ho usato questa domanda come base per un articolo che può essere trovato qui ; vederlo per ulteriori discussioni su questo problema. Grazie per la bella domanda!


Sebbene la risposta di Schabse sia ovviamente corretta e risponda alla domanda che è stata posta, c'è una variante importante della tua domanda che non hai posto:

Cosa succede se i font4 = new Font()lanci dopo che la risorsa non gestita è stata allocata dal costruttore ma prima che il ctor ritorni e compili font4il riferimento?

Permettimi di renderlo un po 'più chiaro. Supponiamo di avere:

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

Ora abbiamo

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

Questo è lo stesso di

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

OK. Supponiamo che Whateverlanci. Quindi il finallyblocco viene eseguito e la risorsa viene deallocata. Nessun problema.

Supponiamo che Blah1()lanci. Quindi il lancio avviene prima che la risorsa venga allocata. L'oggetto è stato assegnato ma il ctor non ritorna mai, quindi foonon viene mai compilato. Non siamo mai entrati in tryquindi non entriamo mai in finallynessuno dei due. Il riferimento all'oggetto è rimasto orfano. Alla fine il GC lo scoprirà e lo metterà nella coda del finalizzatore. handleè ancora zero, quindi il finalizzatore non fa nulla. Si noti che il finalizzatore deve essere robusto di fronte a un oggetto che viene finalizzato il cui costruttore non è mai stato completato . È necessario scrivere finalizzatori così potenti. Questo è un altro motivo per cui dovresti lasciare i finalizzatori di scrittura agli esperti e non provare a farlo da solo.

Supponiamo che Blah3()lanci. Il lancio avviene dopo che la risorsa è stata allocata. Ma ancora una volta, foonon viene mai compilato, non entriamo mai in finallye l'oggetto viene ripulito dal thread del finalizzatore. Questa volta la maniglia è diversa da zero e il finalizzatore la pulisce. Anche in questo caso, il finalizzatore è in esecuzione su un oggetto il cui costruttore non è mai riuscito, ma il finalizzatore viene eseguito comunque. Ovviamente doveva perché questa volta aveva del lavoro da fare.

Supponiamo ora che Blah2()lanci. Il lancio avviene dopo che la risorsa è stata assegnata ma prima handle è stata riempita! Ancora una volta, il finalizzatore verrà eseguito ma ora handleè ancora zero e perdiamo la maniglia!

È necessario scrivere codice estremamente intelligente per evitare che si verifichi questa perdita. Ora, nel caso della tua Fontrisorsa, a chi diavolo importa? Abbiamo perso un font handle, grosso problema. Ma se richiedi in modo assolutamente positivo che ogni risorsa non gestita venga ripulita, indipendentemente dalla tempistica delle eccezioni, allora hai un problema molto difficile tra le mani.

Il CLR deve risolvere questo problema con i blocchi. A partire da C # 4, i blocchi che utilizzano l' lockistruzione sono stati implementati in questo modo:

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

Enterè stato scritto con molta attenzione in modo che, indipendentemente dalle eccezioni generate , lockEnteredsia impostato su true se e solo se il blocco è stato effettivamente preso. Se hai requisiti simili, quello che devi fare è effettivamente scrivere:

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

e scrivi in ​​modo AllocateResourceintelligente in Monitor.Entermodo che, indipendentemente da ciò che accade all'interno AllocateResource, handlevenga compilato se e solo se deve essere deallocato.

Descrivere le tecniche per farlo va oltre lo scopo di questa risposta. Consulta un esperto se hai questo requisito.


6
@gnat: la risposta accettata. Quella S deve rappresentare qualcosa. :-)
Eric Lippert

12
@ Joe: Ovviamente l'esempio è artificioso . L'ho appena inventato . I rischi non sono esagerati perché non ho specificato quale sia il livello di rischio; piuttosto, ho affermato che questo modello è possibile . Il fatto che tu creda che l'impostazione del campo risolva direttamente il problema indica precisamente il mio punto: che come la stragrande maggioranza dei programmatori che non hanno esperienza con questo tipo di problema, non sei competente per risolvere questo problema; anzi, la maggior parte delle persone non riconosce nemmeno che ci sia un problema, motivo per cui ho scritto questa risposta in primo luogo .
Eric Lippert

5
@ Chris: Supponiamo che non ci sia lavoro da fare tra l'assegnazione e la restituzione, e tra la restituzione e l'assegnazione. Eliminiamo tutte quelle Blahchiamate di metodo. Cosa impedisce a ThreadAbortException di verificarsi in uno di questi punti?
Eric Lippert

5
@ Joe: Questa non è una società che discute; Non sto cercando di guadagnare punti essendo più convincente . Se sei scettico e non vuoi credermi sulla parola che questo è un problema complicato che richiede la consultazione di esperti per risolverlo correttamente, allora sei il benvenuto a non essere d'accordo con me.
Eric Lippert

7
@ GilesRoberts: come risolve il problema? Supponiamo che l'eccezione si verifichi dopo la chiamata a AllocateResourcema prima dell'assegnazione a x. A ThreadAbortExceptionquel punto può succedere. Tutti qui sembrano perdere il mio punto, che è la creazione di una risorsa e l'assegnazione di un riferimento ad essa a una variabile non è un'operazione atomica . Per risolvere il problema che ho individuato devi renderlo un'operazione atomica.
Eric Lippert

32

Come complemento alla risposta di @SLaks, ecco l'IL per il tuo codice:

.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

Notare i blocchi try / finalmente annidati.


17

Questo codice (basato sull'esempio originale):

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.
        }
    }
}

Produce il seguente CIL (in Visual Studio 2013 , destinato a .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

Come puoi vedere, il try {}blocco inizia solo dopo la prima assegnazione, che avviene alle IL_0012. A prima vista, questo sembra allocare il primo elemento in codice non protetto. Tuttavia, si noti che il risultato è memorizzato nella posizione 0. Se la seconda allocazione fallisce, il blocco esterno finally {} viene eseguito e questo preleva l'oggetto dalla posizione 0, cioè la prima allocazione di font3, e chiama il suo Dispose()metodo.

È interessante notare che la decompilazione di questo assembly con dotPeek produce la seguente fonte ricostituita:

using System.Drawing;

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

Il codice decompilato conferma che tutto è corretto e che usingè essenzialmente espanso in usings annidati . Il codice CIL è un po 'confuso da guardare, e ho dovuto fissarlo per alcuni minuti buoni prima di capire bene cosa stava succedendo, quindi non sono sorpreso che alcune `` vecchie storie di mogli' 'abbiano iniziato a spuntare su Questo. Tuttavia, il codice generato è la verità inattaccabile.


@Peter Mortensen la tua modifica ha rimosso parti del codice IL (tra IL_0012 e IL_0017) rendendo la spiegazione non valida e confusa. Quel codice doveva essere una copia letterale dei risultati che ho ottenuto e la modifica lo invalida. Puoi rivedere la tua modifica e confermare che questo è ciò che intendevi?
Tim Long,

7

Ecco un codice di esempio per dimostrare la risposta di @SLaks:

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

1
Questo non lo prova. Dov'è Dispose: t2? :)
Piotr Perak

1
La domanda riguarda lo smaltimento della prima risorsa nell'elenco di utilizzo e non della seconda. "Cosa succede se si font4 = new Fontlancia? Da quello che ho capito, font3 perderà risorse e non verrà smaltito."
wdosanjos
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.