web-dev-qa-db-fra.com

"Utiliser" avec plusieurs ressources peut-il provoquer une fuite de ressources?

C # me permet de faire ce qui suit (exemple de MSDN):

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

Que se passe-t-il si font4 = new Font Lance? D'après ce que je comprends, font3 entraînera une fuite de ressources et ne sera pas éliminé.

  • Est-ce vrai? (font4 ne sera pas éliminé)
  • Cela signifie-t-il que using(... , ...) devrait être complètement évité en faveur de l'utilisation imbriquée?
106
Benjamin Gruenbaum

Non.

Le compilateur générera un bloc finally distinct pour chaque variable.

Le spec (§8.13) dit:

Lorsqu'une acquisition de ressource prend la forme d'une déclaration de variable locale, il est possible d'acquérir plusieurs ressources d'un type donné. Une instruction using du formulaire

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

est précisément équivalent à une séquence d'instructions imbriquées:

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

[~ # ~] mise à jour [~ # ~] : J'ai utilisé cette question comme base pour un article qui peut être trouvé ici ; consultez-le pour plus de détails sur cette question. Merci pour la bonne question!


Bien que la réponse de Schabse soit bien sûr correcte et réponde à la question qui a été posée, il y a une variante importante de votre question que vous n'avez pas posée:

Que se passe-t-il si font4 = new Font() jette après la ressource non managée a été allouée par le constructeur mais avant le ctor retourne et remplit font4 avec la référence?

Permettez-moi de clarifier cela un peu. Supposons que nous ayons:

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

Maintenant nous avons

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

C'est la même chose que

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

D'ACCORD. Supposons que Whatever lance. Ensuite, le bloc finally s'exécute et la ressource est désallouée. Aucun problème.

Supposons que Blah1() lance. Ensuite, le lancer se produit avant que la ressource ne soit allouée. L'objet a été alloué mais le ctor ne revient jamais, donc foo n'est jamais rempli. Nous n'avons jamais entré le try donc nous n'entrons jamais non plus le finally. La référence d'objet est devenue orpheline. Finalement, le GC le découvrira et le mettra dans la file d'attente du finaliseur. handle est toujours nul, donc le finaliseur ne fait rien. Notez que le finaliseur doit être robuste face à un objet en cours de finalisation dont le constructeur n'a jamais terminé . Vous êtes tenu d'écrire des finaliseurs aussi solides. C'est une autre raison pour laquelle vous devriez laisser la rédaction des finaliseurs à des experts et ne pas essayer de le faire vous-même.

Supposons que Blah3() lance. Le lancer se produit après l'allocation de la ressource. Mais encore une fois, foo n'est jamais rempli, nous n'entrons jamais dans le finally, et l'objet est nettoyé par le thread du finaliseur. Cette fois, la poignée est différente de zéro et le finaliseur la nettoie. Encore une fois, le finaliseur s'exécute sur un objet dont le constructeur n'a jamais réussi, mais le finaliseur fonctionne quand même. Evidemment ça doit parce que cette fois, ça avait du travail à faire.

Supposons maintenant que Blah2() lance. Le lancer se produit après l'allocation de la ressource mais avanthandle est rempli! Encore une fois, le finaliseur s'exécutera mais maintenant handle est toujours nul et nous perdons la poignée!

Vous devez écrire extrêmement un code intelligent afin d'éviter que cette fuite ne se produise. Maintenant, dans le cas de votre ressource Font, qui s'en soucie? Nous avons une fuite de poignée de police, gros problème. Mais si vous avez absolument besoin que chaque ressource non gérée soit nettoyée quel que soit le moment des exceptions alors vous avez un problème très difficile à résoudre.

Le CLR doit résoudre ce problème avec des verrous. Depuis C # 4, les verrous qui utilisent l'instruction lock ont été implémentés comme ceci:

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

Enter a été très soigneusement écrit afin que quelles que soient les exceptions levées, lockEntered est défini sur true si et uniquement si le verrou a bien été pris. Si vous avez des exigences similaires, vous devez en fait écrire:

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

et écrivez AllocateResource astucieusement comme Monitor.Enter afin que, quoi qu'il se passe à l'intérieur de AllocateResource, le handle est rempli si et seulement si il doit être désalloué.

Décrire les techniques pour le faire dépasse le cadre de cette réponse. Consultez un expert si vous avez cette exigence.

67
Eric Lippert

En complément de la réponse @SLaks, voici l'IL de votre 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

Notez les blocs try/finally imbriqués.

32
David Heffernan

Ce code (basé sur l'exemple d'origine):

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

Il produit ce qui suit CIL (dans Visual Studio 201 , ciblant . 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

Comme vous pouvez le voir, le bloc try {} Ne démarre qu'après la première allocation, qui a lieu à IL_0012. À première vue, cela apparaît pour allouer le premier élément dans le code non protégé. Cependant, notez que le résultat est stocké à l'emplacement 0. Si la deuxième allocation échoue, le bloc externe finally {} S'exécute, et ceci récupère l'objet depuis l'emplacement 0, c'est-à-dire la première allocation de font3, et appelle sa méthode Dispose().

Fait intéressant, la décompilation de cet assembly avec dotPeek produit la source reconstituée suivante:

using System.Drawing;

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

Le code décompilé confirme que tout est correct et que le using est essentiellement développé en usings imbriqués. Le code CIL est un peu déroutant à regarder, et j'ai dû le regarder pendant quelques minutes avant de bien comprendre ce qui se passait, donc je ne suis pas surpris que certains "vieux contes d'épouses" aient commencé à germer cette. Cependant, le code généré est la vérité inattaquable.

17
Tim Long

Voici un exemple de code pour prouver la réponse de @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);
    }
}
7
wdosanjos