Может ли "использование" с несколькими ресурсами вызвать утечку ресурсов?

С# позволяет мне сделать следующее (пример из MSDN):

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

Что произойдет, если font4 = new Font выбрасывает? Из того, что я понимаю, шрифт3 будет утечка ресурсов и не будет удаляться.

  • Это правда? (font4 не будет удаляться)
  • Означает ли это, что using(... , ...) следует избегать вообще в пользу вложенного использования?

Ответ 1

Нет.

Компилятор будет генерировать отдельный блок finally для каждой переменной.

spec (§8.13) говорит:

Когда сбор ресурсов принимает форму local-variable-declaration, можно получить несколько ресурсов данного типа. A using формулировка формы

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

точно эквивалентно последовательности вложенных операторов:

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

Ответ 2

ОБНОВЛЕНИЕ. Я использовал этот вопрос в качестве основы для статьи, которую можно найти здесь; см. дополнительную информацию по этому вопросу. Спасибо за хороший вопрос!


Хотя ответ Schabse, конечно, правильный и отвечает на заданный вопрос, есть важный вариант на ваш вопрос, который вы не задали:

Что произойдет, если font4 = new Font() выдает после того, как неконтролируемый ресурс был назначен конструктором, но до того, как ctor вернется и заполнит font4 ссылкой?

Позвольте мне сделать это немного более ясно. Предположим, что:

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

Теперь мы имеем

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

Это то же самое, что и

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

OK. Предположим, что Whatever выбрасывает. Затем выполняется блок finally, и ресурс освобождается. Нет проблем.

Предположим, что Blah1() выбрасывает. Затем бросок происходит до выделения ресурса. Объект был выделен, но ctor никогда не возвращается, поэтому foo никогда не заполняется. Мы никогда не вводили try, поэтому мы никогда не вводим finally. Ссылка на объект была потеряна. В конце концов GC обнаружит это и поместит его в очередь финализатора. handle по-прежнему равен нулю, поэтому финализатор ничего не делает. Обратите внимание, что финализатор должен быть надежным перед лицом объекта, который завершается, конструктор которого никогда не завершался. Вам требуется , чтобы написать финализаторы, которые являются сильными. Это еще одна причина, по которой вам следует оставлять готовые финализаторы для экспертов и не пытаться делать это самостоятельно.

Предположим, что Blah3() выбрасывает. Бросок происходит после выделения ресурса. Но опять же, foo никогда не заполняется, мы никогда не вводим finally, и объект очищается потоком финализатора. На этот раз дескриптор не равен нулю, и финализатор очистит его. Опять же, финализатор работает на объекте, конструктор которого никогда не удался, но финализатор работает в любом случае. Очевидно, это должно быть, потому что на этот раз у него была работа.

Теперь предположим, что Blah2() выбрасывает. Бросок происходит после выделения ресурса, но до заполнения handle! Опять же, финализатор запустится, но теперь handle все равно ноль, и мы пропустим ручку!

Вам нужно написать чрезвычайно умный код, чтобы предотвратить эту утечку. Теперь, в случае вашего ресурса Font, кого это волнует? Мы просачиваем дескриптор шрифта, большое дело. Но если вы абсолютно положительно требуете, чтобы каждый неуправляемый ресурс был очищен независимо от того, что время исключений, тогда у вас есть очень сложная проблема на ваши руки.

Среда CLR должна решить эту проблему с помощью блокировок. Поскольку С# 4, блокировки, которые используют оператор lock, были реализованы следующим образом:

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

Enter был очень тщательно написан таким образом, чтобы независимо от того, какие исключения выбраны, lockEntered устанавливается в true , если и только если блокировка была фактически выполнена. Если у вас есть аналогичные требования, то то, что вам нужно, на самом деле пишет:

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

и напишите AllocateResource ловко, как Monitor.Enter, так что независимо от того, что происходит внутри AllocateResource, handle заполняется тогда и только тогда, когда нужно его освободить.

Описание методов для этого выходит за рамки этого ответа. Если у вас есть это требование, обратитесь к эксперту.

Ответ 3

В качестве дополнения к ответу @SLaks, здесь IL для вашего кода:

.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

Обратите внимание на вложенные блоки try/finally.

Ответ 4

Этот код (на основе исходного образца):

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

Он производит следующее CILVisual Studio 2013, таргетинг .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

Как вы можете видеть, блок try {} не запускается только после первого выделения, которое имеет место в IL_0012. На первый взгляд это, по-видимому, выделяет первый элемент незащищенного кода. Однако обратите внимание, что результат сохраняется в местоположении 0. Если второе выделение не выполняется, выполняется внешний finally {} блок, и этот выбор выбирается из местоположения 0, т.е. первого выделения font3, и вызывает его Dispose() > .

Интересно, что декомпиляция этой сборки с помощью dotPeek создает следующий восстановленный источник:

using System.Drawing;

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

Декомпилированный код подтверждает, что все правильно, и что using по существу расширен в вложенные using s. Код CIL немного запутан, чтобы смотреть на него, и я должен был смотреть на него в течение нескольких минут, прежде чем я правильно понял, что происходит, поэтому я не удивлен, что некоторые "старые истории жен" начали прорастать это. Однако сгенерированный код является неприступной истиной.

Ответ 5

Вот пример кода, чтобы доказать ответ @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);
    }
}