可以“使用”多个资源导致资源泄漏吗?

时间:2014-01-14 16:08:58

标签: c# using using-statement

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(... , ...)支持嵌套使用?

5 个答案:

答案 0 :(得分:158)

没有

编译器将为每个变量生成一个单独的finally块。

spec(§8.13)说:

  

当资源获取采取的形式   local-variable-declaration,有可能获得多个   给定类型的资源。表单的using声明

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

正是如此   等同于嵌套的using语句序列:

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

答案 1 :(得分:67)

更新:我使用此问题作为可以找到here的文章的基础;请参阅此处以进一步讨论此问题。谢谢你的好问题!


虽然Schabse's answer当然是正确的并且回答了问题,但是您提出的问题有一个重要的变体:

  

如果{/ 1}} 在构造函数分配了非托管资源之后 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抛出。然后在分配资源之前抛出。该对象已被分配但ctor永远不会返回,因此永远不会填写Blah1()。我们从未输入foo所以我们也从未输入try。对象引用已经成为孤立的。最终GC会发现并将其放在终结器队列中。 finally仍为零,因此终结器不执行任何操作。 请注意,面对正在完成其构造函数从未完成的对象,终结器必须是健壮的。您必需来编写强大的终结器。这也是为什么你应该把写作终结者留给专家而不是试图自己做的另一个原因。

假设handle抛出。抛出在资源分配后发生。但同样,Blah3()永远不会被填充,我们永远不会进入foo,并且终结器线程会清理对象。这次句柄非零,终结器清理它。同样,终结器在一个对象上运行,该对象的构造函数从未成功,但终结器仍在运行。显然它必须因为这一次,它有工作要做。

现在假设finally抛出。在分配资源之后抛出,但在 Blah2()填充之前!同样,终结器将运行,但现在handle仍为零,我们泄漏了句柄!

您需要编写聪明的代码,以防止发生此类泄漏。现在,就你的handle资源而言,他究竟关心谁?我们泄漏了一个字体句柄,很重要。但是,如果您绝对肯定要求 每个非托管资源都要清理,无论例外的时间是什么,那么您就会遇到一个非常棘手的问题你的双手。

CLR必须通过锁来解决这个问题。从C#4开始,使用Font语句的锁已经实现如下:

lock

bool lockEntered = false; object lockObject = whatever; try { Monitor.Enter(lockObject, ref lockEntered); lock body here } finally { if (lockEntered) Monitor.Exit(lockObject); } 已经非常仔细地编写,以便无论抛出什么异常Enter设置为true 当且仅当锁实际上已被采取。如果您有类似的要求,那么您需要实际写的是:

lockEntered

然后将 public Foo() { Blah1(); AllocateResource(ref handle); Blah2(); Blah3(); } 巧妙地写为AllocateResource,这样无论Monitor.Enter内发生了什么,AllocateResource都填入当且仅当它需要被解除分配。

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

答案 2 :(得分: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块。

答案 3 :(得分: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基本上扩展为嵌套using。 CIL代码看起来有点令人困惑,在我正确理解发生的事情之前,我必须盯着它看几分钟,所以我并不惊讶一些“老太太的故事”已经开始萌芽了这个。但是,生成的代码是无懈可击的事实。

答案 4 :(得分: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);
    }
}