“使用”多个资源是否会导致资源泄漏?


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 2014年

3
无论如何,如果using(... , ...)使用块将其编译到嵌套中,我都不会感到惊讶,但是我不确定。
Dan J

1
我不是这个意思。即使您根本不使用using,GC最终仍会收集它。
SLaks 2014年

1
@zneak:如果将其编译为单个finally块,则在构造所有资源之前,它不会进入该块。
SLaks 2014年

2
@zneak:因为在一个转换usingtry- finally,初始化表达外的评价try。因此,这是一个合理的问题。
Ben Voigt 2014年

Answers:


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

4
在C#规范版本5.0(顺便说一句)中为8.13。
Ben Voigt 2014年

11
@WeylandYutani:你在问什么?
SLaks 2014年

9
@WeylandYutani:这是一个问答网站。如果您有问题,请开始一个新的问题!
埃里克·利珀特

5
@ user1306322为什么?如果我真的想知道怎么办?
Oxymoron 2014年

2
@Oxymoron然后,您应该在以研究和猜测的形式发布问题之前,提供一些努力的证据,否则,您将被告知相同,失去注意力或付出更大的损失。只是基于个人经验的建议。
user1306322 2014年

67

UPDATE:我用这个问题作为可以找到一篇文章的基础在这里 ; 看到它以进一步讨论此问题。感谢您的好问题!


尽管Schabse的答案当然是正确的,并且可以回答所提出的问题,但是您没有提出的问题有一个重要的变体:

如果在构造函数分配了非托管资源之后 ctor返回并填充引用之前font4 = new Font()抛出该异常怎么办?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一个。对象引用已被孤立。最终,GC将发现该问题并将其放入终结器队列中。 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();
    }

AllocateResourceMonitor.Enter这样巧妙地编写,以便无论内部发生什么情况AllocateResource只要且仅当需要重新分配时,才进行handle填充。

描述这样做的技术超出了此答案的范围。如果您有此要求,请咨询专家。


6
@gnat:可接受的答案。S必须代表某种东西。:-)
Eric Lippert 2014年

12
@Joe:当然是人为的例子。 我只是做作。风险没有被夸大,因为我没有说风险水平是多少。相反,我已经说过这种模式是可能的。您认为直接设置领域可以解决问题的事实恰好表明了我的观点:就像绝大多数没有此类问题经验的程序员一样,您无能力解决此问题;事实上,大多数人甚至不承认存在一个问题,这就是为什么我首先写了这个答案
埃里克·利珀特

5
@Chris:假设在分配和返回之间以及返回和分配之间完成了零工作。我们删除所有这些Blah方法调用。是什么阻止了ThreadAbortException在任何一个时刻发生?
埃里克·利珀特

5
@乔:这不是一个辩论的社会。我不是想通过说服力来得分。如果您对此表示怀疑,并且不想让我相信这是一个棘手的问题,需要咨询专家以正确解决,那么欢迎您不同意我的看法。
埃里克·利珀特

7
@GilesRoberts:那如何解决问题?假设例外发生呼叫之后AllocateResource但指派给之前x。此时ThreadAbortException可能会发生A。这里的每个人似乎都没有抓住我的观点,即创建资源并将对它的引用分配给变量不是原子操作。为了解决该问题,我已经确定您必须使其成为原子操作。
埃里克·利珀特

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

它将生成以下CIL(在Visual 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。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);
    }
}

1
那并不能证明这一点。处置在哪里:t2?:)
Piotr Perak

1
问题是关于第一个资源在使用列表上的配置,而不是第二个。 “如果font4 = new Font抛出该怎么办?据我所知,font3会泄漏资源并且不会被丢弃。”
wdosanjos 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.