Czy „używanie” więcej niż jednego zasobu może spowodować wyciek zasobów?

106

C # pozwala mi wykonać następujące czynności (przykład z MSDN):

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

Co się stanie, jeśli font4 = new Fontwyrzuci? Z tego co rozumiem, font3 będzie wyciekać zasoby i nie zostanie usunięty.

  • Czy to prawda? (font4 nie zostanie usunięty)
  • Czy to oznacza, że using(... , ...)należy całkowicie unikać na korzyść stosowania zagnieżdżonego?
Benjamin Gruenbaum
źródło
7
Nie wycieknie pamięć; w najgorszym przypadku nadal otrzyma GC.
SLaks
3
Nie zdziwiłbym się, gdyby using(... , ...)był kompilowany do zagnieżdżonych bloków, niezależnie od tego, ale nie wiem tego na pewno.
Dan J
1
Nie o to mi chodzilo. Nawet jeśli w ogóle nie używasz using, GC ostatecznie go odbierze.
SLaks
1
@zneak: Gdyby został skompilowany do pojedynczego finallybloku, nie wszedłby do bloku, dopóki wszystkie zasoby nie zostałyby skonstruowane.
SLaks
2
@zneak: Ponieważ podczas konwersji a usingna try- finally, wyrażenie inicjalizacji jest oceniane poza try. Jest to więc rozsądne pytanie.
Ben Voigt

Odpowiedzi:

158

Nie.

Kompilator wygeneruje oddzielny finallyblok dla każdej zmiennej.

Spec (§8.13) mówi:

Gdy pozyskiwanie zasobów ma postać deklaracji zmiennej lokalnej, możliwe jest pozyskanie wielu zasobów danego typu. usingZestawienie postaci

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

jest dokładnie odpowiednikiem sekwencji zagnieżdżonych instrukcji using:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
źródło
4
To jest 8,13 w specyfikacji języka C # w wersji 5.0, przy okazji.
Ben Voigt,
11
@WeylandYutani: O co pytasz?
SLaks
9
@WeylandYutani: To jest witryna z pytaniami i odpowiedziami. Jeśli masz pytanie, zacznij nowe pytanie!
Eric Lippert
5
@ user1306322 dlaczego? A jeśli naprawdę chcę wiedzieć?
Oxymoron
2
@Oxymoron, więc przed wysłaniem pytania powinieneś przedstawić dowody wysiłku w formie badań i domysłów, w przeciwnym razie zostaniesz poinformowany to samo, stracisz uwagę i w przeciwnym razie poniesiesz większą stratę. Porada oparta na osobistym doświadczeniu.
user1306322
67

AKTUALIZACJA : Użyłem tego pytania jako podstawy do artykułu, który można znaleźć tutaj ; zobacz to, aby uzyskać dodatkowe omówienie tego problemu. Dzięki za dobre pytanie!


Chociaż odpowiedź Schabse jest oczywiście poprawna i odpowiada na zadane pytanie, istnieje ważny wariant twojego pytania, którego nie zadałeś:

Co się stanie, jeśli font4 = new Font()wyrzuca po tym, jak niezarządzany zasób został przydzielony przez konstruktora, ale zanim ctor zwróci i wypełni font4odwołanie?

Pozwólcie, że wyjaśnię to trochę. Załóżmy, że mamy:

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

Teraz mamy

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

To jest to samo co

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

DOBRZE. Załóżmy, że Whateverrzuca. Następnie finallyblok jest uruchamiany i zasób jest zwalniany. Nie ma problemu.

Załóżmy, że Blah1()rzuca. Następnie rzut ma miejsce, zanim zasób zostanie przydzielony. Obiekt został przydzielony, ale ctor nigdy nie wraca, więc foonigdy nie jest wypełniany. Nigdy nie tryweszliśmy do pola, więc nigdy nie wchodzimy do finallyżadnego. Odniesienie do obiektu zostało osierocone. W końcu GC to wykryje i umieści to w kolejce finalizatora. handlenadal wynosi zero, więc finalizator nic nie robi. Zwróć uwagę, że finalizator musi być niezawodny w obliczu finalizowanego obiektu, którego konstruktor nigdy nie został ukończony . Jesteś zobowiązany napisać finalizatory że są to silne. To kolejny powód, dla którego powinieneś pozostawić pisanie finalizatorów ekspertom i nie próbować robić tego samodzielnie.

Załóżmy, że Blah3()rzuca. Rzut następuje po przydzieleniu zasobu. Ale znowu, foonigdy nie jest wypełniony, nigdy nie wchodzimy do finally, a obiekt jest czyszczony przez wątek finalizatora. Tym razem uchwyt jest niezerowy, a finalizator czyści go. Ponownie finalizator działa na obiekcie, którego konstruktor nigdy się nie powiódł, ale finalizator i tak działa. Oczywiście musiało, bo tym razem miało do wykonania pracę.

Teraz załóżmy, że Blah2()rzuca. Rzut następuje po przydzieleniu zasobu, ale przed handle wypełnieniem! Ponownie finalizator będzie działał, ale teraz handlenadal wynosi zero i wyciekamy uchwyt!

Musisz napisać niezwykle sprytny kod, aby zapobiec wystąpieniu tego wycieku. Teraz, w przypadku twojego Fontzasobu, kogo to obchodzi? Wyciekamy uchwyt czcionki, wielka sprawa. Ale jeśli absolutnie stanowczo wymagasz, aby każdy niezarządzany zasób został wyczyszczony, bez względu na czas wyjątków , masz bardzo trudny problem.

CLR musi rozwiązać ten problem za pomocą zamków. Od czasu C # 4 blokady, które używają lockinstrukcji, zostały zaimplementowane w następujący sposób:

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

Enterzostał bardzo starannie napisany, aby bez względu na to, jakie wyjątki zostały wyrzucone , lockEnteredjest ustawiony na wartość true wtedy i tylko wtedy, gdy blokada została faktycznie podjęta. Jeśli masz podobne wymagania, to tak naprawdę musisz napisać:

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

i AllocateResourcesprytnie pisz Monitor.Entertak, że bez względu na to, co dzieje się w środku AllocateResource, pole handlejest wypełnione wtedy i tylko wtedy, gdy trzeba je cofnąć.

Opis technik służących do tego wykracza poza zakres tej odpowiedzi. Skonsultuj się z ekspertem, jeśli masz takie wymaganie.

Eric Lippert
źródło
6
@gnat: Zaakceptowana odpowiedź. To S musi coś oznaczać. :-)
Eric Lippert
12
@Joe: Oczywiście przykład jest wymyślony . Po prostu to wymyśliłem . Ryzyko nie jest wyolbrzymione, ponieważ nie podałem, jaki jest poziom ryzyka; raczej stwierdziłem, że ten wzór jest możliwy . Fakt, że uważasz, że ustawienie pola bezpośrednio rozwiązuje problem, wskazuje dokładnie na mój punkt widzenia: że podobnie jak zdecydowana większość programistów, którzy nie mają doświadczenia z tego rodzaju problemami, nie jesteś kompetentny do rozwiązania tego problemu; Rzeczywiście, większość ludzi nawet nie uznają, że nie jest to problem, który jest dlaczego napisałem tę odpowiedź w pierwszej kolejności .
Eric Lippert
5
@Chris: Załóżmy, że między alokacją a zwrotem oraz między zwrotem a przypisaniem nie wykonano żadnej pracy. Usuwamy wszystkie Blahwywołania metod. Co powstrzymuje wystąpienie wyjątku ThreadAbortException w którymkolwiek z tych punktów?
Eric Lippert
5
@Joe: To nie jest społeczeństwo debatujące; Nie chcę zdobywać punktów, będąc bardziej przekonującym . Jeśli jesteś sceptyczny i nie chcesz uwierzyć mi na słowo, że jest to trudny problem, który wymaga konsultacji z ekspertami w celu prawidłowego rozwiązania, możesz się ze mną nie zgodzić.
Eric Lippert
7
@GilesRoberts: Jak to rozwiązuje problem? Załóżmy, że wyjątek występuje po wywołaniu do, AllocateResourceale przed przypisaniem do x. W ThreadAbortExceptiontym momencie może się wydarzyć. Wydaje się, że wszystkim tutaj brakuje mojego punktu widzenia, jakim jest stworzenie zasobu i przypisanie do niego odniesienia do zmiennej nie jest operacją atomową . Aby rozwiązać problem, który zidentyfikowałem, musisz uczynić z tego operację atomową.
Eric Lippert
32

Jako uzupełnienie odpowiedzi @SLaks, oto IL dla twojego kodu:

.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

Zwróć uwagę na zagnieżdżone bloki try / final.

David Heffernan
źródło
17

Ten kod (na podstawie oryginalnej próbki):

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

Tworzy następujący CIL (w programie Visual Studio 2013 , przeznaczony dla .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

Jak widać, try {}blok zaczyna się dopiero po pierwszej alokacji, która ma miejsce o godz IL_0012. Na pierwszy rzut oka wydaje się, że przydziela to pierwszy element w niezabezpieczonym kodzie. Należy jednak zauważyć, że wynik jest przechowywany w lokalizacji 0. Jeśli następnie druga alokacja nie powiedzie się, wykonywany jest blok zewnętrzny finally {} , a to pobiera obiekt z lokalizacji 0, tj. Pierwszej alokacji font3, i wywołuje jego Dispose()metodę.

Co ciekawe, dekompilacja tego zestawu za pomocą dotPeek daje następujące odtworzone źródło:

using System.Drawing;

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

Zdekompilowany kod potwierdza, że ​​wszystko jest poprawne i że usingjest zasadniczo rozszerzony do zagnieżdżonych plików usings. Kod CIL jest trochę zagmatwany i musiałem się w niego wpatrywać przez dobre kilka minut, zanim właściwie zrozumiałem, co się dzieje, więc nie jestem zaskoczony, że zaczęły się pojawiać `` opowieści starych żon '' to. Jednak wygenerowany kod jest niepodważalną prawdą.

Tim Long
źródło
@Peter Mortensen twoja edycja usunęła fragmenty kodu IL (między IL_0012 a IL_0017), czyniąc wyjaśnienie zarówno niepoprawnym, jak i mylącym. Ten kod miał być dosłowną kopią wyników, które uzyskałem, a edycja to unieważnia. Czy możesz przejrzeć swoją zmianę i potwierdzić, że jest to zamierzone?
Tim Long
7

Oto przykładowy kod potwierdzający odpowiedź @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
źródło
1
To nie dowodzi. Gdzie jest Dispose: t2? :)
Piotr Perak
1
Pytanie dotyczy pozbycia się pierwszego zasobu na liście wykorzystujących, a nie drugiego. „Co się stanie, jeśli font4 = new Fontwyrzuci? Z tego, co rozumiem, font3 spowoduje wyciek zasobów i nie zostanie usunięty”.
wdosanjos