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

Ben*_*aum 106 c# using using-statement

C#允许我执行以下操作(来自MSDN的示例):

using (Font font3 = new Font("Arial", 10.0f),
            font4 = new Font("Arial", 10.0f))
{
    // Use font3 and font4.
}
Run Code Online (Sandbox Code Playgroud)

如果发生什么font4 = new Font抛出?从我的理解,font3将泄漏资源,不会被处置.

  • 这是真的?(font4不会被处理掉)
  • 这是否using(... , ...)应该完全避免使用嵌套使用?

SLa*_*aks 158

没有.

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

规范(§8.13)说:

当资源获取采用局部变量声明的形式时,可以获取给定类型的多个资源.一个using形式的声明

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement 
Run Code Online (Sandbox Code Playgroud)

恰好等同于嵌套的using语句序列:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
Run Code Online (Sandbox Code Playgroud)

  • @WeylandYutani:你在问什么? (11认同)
  • @WeylandYutani:这是一个问答网站.如果您有疑问,请开始提出新问题! (9认同)
  • @ user1306322为什么?如果我真的想知道怎么办? (5认同)
  • 这是C#规范版本5.0中的8.13,顺便说一下. (4认同)
  • @Oxymoron然后你应该在以研究和猜测的形式发布问题之前提供一些努力的证据,否则你会被告知相同的,失去注意力,否则会更大的损失.只是基于个人经验的建议. (2认同)

Eri*_*ert 67

更新:我用这个问题作为一篇文章的基础,可以在这里找到; 请参阅此处以进一步讨论此问题.谢谢你的好问题!


虽然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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们有

using(Foo foo = new Foo())
    Whatever(foo);
Run Code Online (Sandbox Code Playgroud)

这是一样的

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

好.假设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);
}
Run Code Online (Sandbox Code Playgroud)

Enter已经非常仔细地编写,因此无论抛出什么异常,当且仅当实际执行锁定时才lockEntered设置为true .如果您有类似的要求,那么您需要实际写入:

    public Foo()
    {
        Blah1();
        AllocateResource(ref handle);
        Blah2();
        Blah3();
    }
Run Code Online (Sandbox Code Playgroud)

AllocateResource巧妙地写Monitor.Enter出来,无论内部发生什么AllocateResource,当且仅当需要解除分配时,才会handle填写.

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

  • @Joe:当然这个例子是*人为的*.**我只是设法**.风险不夸张*因为我没有说明风险的*等级*是什么; 相反,我已经声明这种模式是*可能*.你认为设置字段直接解决问题的事实恰恰表明了我的观点:就像绝大多数没有这类问题经验的程序员一样,你无法解决这个问题; 事实上,大多数人甚至不认识到*是*问题,这就是为什么我在第一时间写这个答案*. (12认同)
  • @GilesRoberts:这是如何解决问题的?假设异常发生在*调用`AllocateResource`之后*但*之前*赋值给`x`.那时可能发生`ThreadAbortException`.这里的每个人似乎都忽略了我的观点,即**创建资源并将其引用赋值给变量不是原子操作**.为了解决我已经确定的问题,你必须使它成为一个原子操作. (7认同)
  • @gnat:接受的答案.S必须代表什么.:-) (6认同)
  • @Chris:假设在分配和返回之间以及返回和赋值之间没有完成工作.我们删除所有那些`Blah`方法调用.**什么阻止ThreadAbortException在这两个点发生?** (5认同)
  • @Joe:这不是辩论社会; 我不打算通过更有说服力来获得积分*.如果你持怀疑态度并且不想接受我的话,这是一个棘手的问题,需要咨询专家才能正确解决,欢迎你不同意我的意见. (5认同)
  • 有些背景,我们正在研究[`for` for JavaScript promises](https://github.com/petkaantonov/bluebird/issues/65).昨天我们进行了完全相同的讨论(构造函数分配资源然后抛出),我们决定_explicitly_要求构造函数(我们的模拟函数是在资源上返回一个promise的函数(比如你不熟悉这个术语的任务))没有抛出保证(JS中没有析构函数).我们有相当困难的时间:),想想`使用(var resources = await ObtainUsingTaskWhenAll(t1,t2,t3,t4)){` (4认同)
  • @gbjbaanb:来自某个地方的半引用:"在软件方面,任何百万分之一的事情都会发生在每秒一次." (3认同)
  • @BenjaminGruenbaum:听起来像一个有趣的项目!是的,我熟悉承诺. (3认同)
  • @EricLippert完全不能完全防御ThreadAbort?无论你做什么,线程都可以在你做之前中止.(或什么http://msdn.microsoft.com/en-us/library/ms228973(v=vs.110).aspx帮助?) (3认同)
  • @slaks确实可以使用CER. (3认同)
  • @EricLippert谢谢.我花了一段时间阅读你的评论来解决这个问题.你是对的,我假设x = AllocateResource(); 是原子操作.至少我希望摆脱那些不会认识到这是一个问题的程序员.我仍然是那些无法解决这个问题的程序员.我有兴趣了解这个问题的解决方案.TIL我的异常处理代码并不像我想象的那么强大. (3认同)
  • *我们泄漏了一个字体句柄,大不了*...我发现,如果你泄漏1个字体句柄,你的代码是这样的,它会经常泄漏字体句柄......到它崩溃可怕的程度.看到它发生:( (2认同)
  • @Chris:正确.这就是为什么我们有'SafeHandle`. (2认同)

Dav*_*nan 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
Run Code Online (Sandbox Code Playgroud)

注意嵌套的try/finally块.


Tim*_*ong 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.
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它生成以下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
Run Code Online (Sandbox Code Playgroud)

如您所见,该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))
                ;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

反编译的代码确认一切都是正确的,并且using基本上扩展为嵌套的usings.CIL代码看起来有点令人困惑,在我正确理解发生的事情之前,我必须盯着它看几分钟,所以我并不惊讶一些"老太太的故事"已经开始萌芽了这个.但是,生成的代码是无懈可击的事实.


wdo*_*jos 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);
    }
}
Run Code Online (Sandbox Code Playgroud)