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

106

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 Fontjette? D'après ce que je comprends, font3 perdra des ressources et ne sera pas éliminé.

  • Est-ce vrai? (font4 ne sera pas éliminé)
  • Est-ce que ce moyen using(... , ...)doit être évité en faveur d'une utilisation imbriquée?
Benjamin Gruenbaum
la source
7
Il ne fuira pas de mémoire; dans le pire des cas, il obtiendra toujours GC'd.
SLaks le
3
Je ne serais pas surpris s'il using(... , ...)est compilé en blocs à l'aide de blocs imbriqués, mais je ne le sais pas avec certitude.
Dan J
1
Ce n'est pas ce que je voulais dire. Même si vous ne l'utilisez pas usingdu tout, le GC finira par le récupérer.
SLaks
1
@zneak: S'il avait été compilé en un seul finallybloc, il ne serait pas entré dans le bloc tant que toutes les ressources n'ont pas été construites.
SLaks
2
@zneak: Parce que dans la conversion de a usingen a try- finally, l'expression d'initialisation est évaluée en dehors du try. C'est donc une question raisonnable.
Ben Voigt

Réponses:

158

Non.

Le compilateur générera un finallybloc séparé pour chaque variable.

La spécification (§8.13) dit:

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

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

équivaut précisément à une séquence d'instructions using imbriquées:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
la source
4
C'est 8.13 dans la version 5.0 de la spécification C #, btw.
Ben Voigt le
11
@WeylandYutani: Que demandez-vous?
SLaks le
9
@WeylandYutani: Ceci est un site de questions et réponses. Si vous avez une question, commencez une nouvelle question s'il vous plaît!
Eric Lippert
5
@ user1306322 pourquoi? Et si je veux vraiment savoir?
Oxymoron
2
@Oxymoron, vous devriez alors fournir des preuves d'efforts avant de publier la question sous forme de recherche et de suppositions, sinon on vous dira la même chose, perdez votre attention et soyez autrement plus perdu. Juste un conseil basé sur une expérience personnelle.
user1306322
67

MISE À JOUR : J'ai utilisé cette question comme base pour un article qui peut être trouvé ici ; le voir pour une discussion supplémentaire 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 existe une variante importante de votre question que vous n'avez pas posée:

Que se passe-t-il si se font4 = new Font()lance après que la ressource non gérée a été allouée par le constructeur mais avant que le ctor ne retourne et remplisse font4la référence?

Permettez-moi de clarifier cela un peu plus. 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 des Whateverlancers. Ensuite, le finallybloc s'exécute et la ressource est désallouée. Aucun problème.

Supposons des Blah1()lancers. 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 il foon'est jamais renseigné. Nous n'avons jamais entré le trydonc nous n'entrons jamais le finallynon plus. 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. handleest toujours zéro, donc le finaliseur ne fait rien. Notez que le finaliseur doit être robuste face à un objet en cours de finalisation dont le constructeur n'est jamais terminé . Vous devez écrire finaliseurs qui sont ce fort. C'est encore 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 des Blah3()lancers. Le lancer se produit après l'allocation de la ressource. Mais encore une fois, foon'est jamais renseigné, nous n'entrons jamais dans le finally, et l'objet est nettoyé par le thread de finalisation. Cette fois, la poignée est différente de zéro et le finaliseur la nettoie. Là encore, le finaliseur s'exécute sur un objet dont le constructeur n'a jamais réussi, mais le finaliseur s'exécute quand même. Evidemment il le faut car cette fois, il avait du travail à faire.

Supposons maintenant des Blah2()lancers. Le lancer se produit après l'allocation de la ressource, mais avant le handle remplissage! Encore une fois, le finaliseur fonctionnera mais maintenant il handleest toujours à zéro et nous perdons la poignée!

Vous devez écrire un code extrêmement intelligent pour éviter que cette fuite ne se produise. Maintenant, dans le cas de votre Fontressource, qui s'en soucie? Nous perdons une poignée de police, gros problème. Mais si vous exigez absolument que toutes les ressources non gérées soient nettoyées, quel que soit le moment des exceptions, vous avez un problème très difficile entre les mains.

Le CLR doit résoudre ce problème avec les verrous. Depuis C # 4, les verrous qui utilisent l' lockinstruction 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);
}

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

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

et écrivez AllocateResourceintelligemment Monitor.Enterpour que quoi qu'il se passe à l'intérieur AllocateResource, le handlesoit rempli si et seulement s'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.

Eric Lippert
la source
6
@gnat: La réponse acceptée. Ce S doit représenter quelque chose. :-)
Eric Lippert
12
@Joe: Bien sûr, l'exemple est artificiel . Je viens de l'inventer . Les risques ne sont pas exagérés parce que je n'ai pas indiqué quel est le niveau de risque; plutôt, j'ai déclaré que ce modèle est possible . Le fait que vous croyiez que définir le champ résout directement le problème indique précisément mon point: que comme la grande majorité des programmeurs qui n'ont aucune expérience avec ce genre de problème, vous n'êtes pas compétent pour résoudre ce problème; En effet, la plupart des gens ne reconnaissent même pas qu'il y a un problème, ce qui est la raison pour laquelle je l' ai écrit cette réponse en premier lieu .
Eric Lippert
5
@Chris: Supposons qu'il n'y ait aucun travail effectué entre l'allocation et le retour, et entre le retour et l'affectation. Nous supprimons tous ces Blahappels de méthode. Qu'est-ce qui empêche une ThreadAbortException de se produire à l'un ou l'autre de ces points?
Eric Lippert
5
@Joe: Ce n'est pas une société de débat; Je ne cherche pas à marquer des points en étant plus convaincant . Si vous êtes sceptique et que vous ne voulez pas me croire sur parole, il s'agit d'un problème délicat qui nécessite une consultation avec des experts pour être résolu correctement, alors vous êtes invités à ne pas être d'accord avec moi.
Eric Lippert
7
@GilesRoberts: Comment cela résout-il le problème? Supposons que l'exception se produise après l'appel à AllocateResourcemais avant l'affectation à x. Un ThreadAbortExceptionpeut se produire à ce stade. Tout le monde ici semble manquer mon point, qui est la création d'une ressource et l'attribution d'une référence à celle-ci à une variable n'est pas une opération atomique . Afin de résoudre le problème que j'ai identifié, vous devez en faire une opération atomique.
Eric Lippert
32

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.

David Heffernan
la source
17

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 le CIL suivant (dans Visual Studio 2013 , 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 try {}blocage ne démarre qu'après la première allocation, qui a lieu à IL_0012. À première vue, cela semble allouer le premier élément en code non protégé. Cependant, notez que le résultat est stocké à l'emplacement 0. Si la deuxième allocation échoue alors, le bloc externe finally {} s'exécute, et cela récupère l'objet à l'emplacement 0, c'est-à-dire la première allocation de font3, et appelle sa Dispose()méthode.

Fait intéressant, la décompilation de cet assemblage 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 usingest essentiellement développé en usings imbriqués . Le code CIL est un peu déroutant à regarder, et j'ai dû le regarder pendant quelques bonnes minutes avant de bien comprendre ce qui se passait, donc je ne suis pas surpris que des `` histoires de vieilles femmes '' aient commencé à germer à propos de ce. Cependant, le code généré est la vérité inattaquable.

Tim Long
la source
@Peter Mortensen votre édition a supprimé des morceaux du code IL (entre IL_0012 et IL_0017) rendant l'explication à la fois invalide et déroutante. Ce code était destiné à être une copie textuelle des résultats que j'ai obtenus et l'édition l'invalide. Pouvez-vous s'il vous plaît revoir votre modification et confirmer que c'est ce que vous vouliez?
Tim Long
7

Voici un exemple de code pour prouver la réponse @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);
    }
}
wdosanjos
la source
1
Cela ne le prouve pas. Où est Dispose: t2? :)
Piotr Perak
1
La question porte sur l'élimination de la première ressource de la liste d'utilisation et non de la seconde. "Que se passe-t-il si font4 = new Fontjette? D'après ce que j'ai compris, font3 perdra des ressources et ne sera pas éliminé."
wdosanjos