Может ли «использование» более чем одного ресурса вызвать утечку ресурса?

106

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

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

Что будет, если font4 = new Fontбросит? Насколько я понимаю, у font3 будут утечки ресурсов, и он не будет утилизирован.

  • Это правда? (font4 не будет утилизирован)
  • Означает ли это, что using(... , ...)следует вообще избегать использования вложенного кода?
Бенджамин Грюнбаум
источник
7
Это не приведет к утечке памяти; в худшем случае он все равно получит GC.
SLaks
3
Я не удивлюсь, если он using(... , ...)будет скомпилирован во вложенные блоки, но я этого точно не знаю.
Dan J
1
Это не то, что я имел ввиду. Даже если вы не используете его usingвообще, GC все равно его заберет.
SLaks
1
@zneak: Если бы он был скомпилирован в один finallyблок, он не попал бы в блок, пока не были созданы все ресурсы.
SLaks
2
@zneak: потому что при преобразовании a usingв try- finallyвыражение инициализации оценивается вне try. Так что это разумный вопрос.
Ben Voigt

Ответы:

158

Нет.

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

В спецификации (§8.13) говорится:

Когда получение ресурса принимает форму объявления локальной переменной, можно получить несколько ресурсов заданного типа. usingУтверждение вида

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

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

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
источник
4
Это 8.13 в спецификации C # версии 5.0, кстати.
Ben Voigt
11
@WeylandYutani: Что ты спрашиваешь?
SLaks
9
@WeylandYutani: Это сайт вопросов и ответов. Если у вас есть вопрос, задайте новый вопрос!
Эрик Липперт
5
@ user1306322 почему? Что, если я действительно хочу знать?
Oxymoron
2
@Oxymoron, тогда вы должны предоставить некоторые доказательства усилий, прежде чем размещать вопрос в форме исследования и предположений, иначе вам скажут то же самое, вы потеряете внимание и в противном случае потеряете больше. Просто совет, основанный на личном опыте.
user1306322
67

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


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

Что произойдет, если 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();
    }
}

ХОРОШО. Допустим Whateverкидает. Затем finallyблок запускается, и ресурс освобождается. Нет проблем.

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

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

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

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

CLR должна решить эту проблему с помощью блокировок. Начиная с C # 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заполнялось тогда и только тогда, когда его нужно освободить.

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

Эрик Липперт
источник
6
@gnat: принятый ответ. Это S должно что-то обозначать. :-)
Эрик Липперт
12
@Joe: Конечно, пример надуманный . Я просто это придумал . Риски не преувеличены, потому что я не указал, каков уровень риска; скорее, я заявил, что этот образец возможен . Тот факт, что вы полагаете, что установка поля напрямую решает проблему, указывает именно на мою точку зрения: как и подавляющее большинство программистов, не имеющих опыта решения такого рода проблем, вы не способны решить эту проблему; на самом деле, большинство людей даже не признают , что это проблема, которая почему я написал этот ответ в первую очередь .
Эрик Липперт
5
@Chris: Предположим, что между распределением и возвратом, а также между возвратом и присвоением не выполняется нулевая работа. Мы удаляем все эти Blahвызовы методов. Что мешает возникновению ThreadAbortException в любой из этих точек?
Эрик Липперт
5
@Joe: Это не дискуссионное общество; Я не собираюсь набирать очки, будучи более убедительным . Если вы настроены скептически и не хотите верить мне на слово, что это сложная проблема, для правильного решения которой требуется консультация экспертов, вы можете не согласиться со мной.
Эрик Липперт
7
@GilesRoberts: Как это решает проблему? Предположим, что исключение происходит после вызова, AllocateResourceно до присвоения x. В этот момент ThreadAbortExceptionможет произойти А. Кажется, что все здесь упускают мою точку зрения, а именно: создание ресурса и присвоение ссылки на него переменной не является атомарной операцией . Чтобы решить указанную мной проблему, вы должны сделать ее атомарной операцией.
Эрик Липперт
32

В дополнение к ответу @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.

Дэвид Хеффернан
источник
17

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

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существу расширен во вложенные usings. Код CIL немного сбивает с толку, и мне пришлось смотреть на него в течение нескольких минут, прежде чем я правильно понял, что происходит, поэтому я не удивлен, что некоторые `` сказки старых жен '' начали прорастать вокруг этот. Однако сгенерированный код - неопровержимая правда.

Тим Лонг
источник
@Peter Mortensen: ваше редактирование удалило фрагменты кода IL (между IL_0012 и IL_0017), что сделало объяснение недействительным и запутанным. Этот код был задуман как дословная копия полученных мной результатов, и редактирование делает это недействительным. Не могли бы вы просмотреть свою правку и подтвердить, что это то, что вы намеревались сделать?
Тим Лонг
7

Вот пример кода, подтверждающий ответ @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
источник
1
Это не доказывает. Где находится утилизация: t2? :)
Петр Перак
1
Вопрос в том, чтобы избавиться от первого ресурса в списке использования, а не от второго. «Что произойдет, если font4 = new Fontвыкинет? Насколько я понимаю, в font3 будет утечка ресурсов, и он не будет удален».
wdosanjos