web-dev-qa-db-ja.com

複数のリソースを「使用」すると、リソースリークが発生しますか?

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(... , ...)を完全に回避する必要があるということですか?
106

番号。

コンパイラーは、変数ごとに個別のfinallyブロックを生成します。

spec (§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
158
SLaks

[〜#〜] update [〜#〜]:見つけられる記事の基礎としてこの質問を使用しました here ;この問題の詳細については、それを参照してください。良い質問をありがとう!


Schabseの答え はもちろん正しいものであり、尋ねられた質問に答えますが、あなたが尋ねなかった質問には重要なバリエーションがあります:

font4 = new Font()afterをスローした場合に起こること---アンマネージリソースはコンストラクターによって割り当てられましたが、before 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はまだゼロなので、ファイナライザは何もしません。 ファイナライザは、コンストラクタが完了しなかったファイナライズされるオブジェクトに直面して堅牢である必要があることに注意してください。あなたは、この強力なファイナライザを書くためにrequiredです。これは、ファイナライザの作成を専門家に任せ、自分でやろうとしない別の理由です。

Blah3()がスローするとします。リソースが割り当てられた後にスローが発生します。しかし、再び、fooは入力されず、finallyを入力することはなく、オブジェクトはファイナライザースレッドによってクリーンアップされます。今回はハンドルがゼロ以外であり、ファイナライザがそれをクリーンアップします。繰り返しますが、ファイナライザはコンストラクタが成功しなかったオブジェクトで実行されていますが、ファイナライザはとにかく実行されます。明らかに、今回はやるべき仕事があったからです。

Blah2()がスローするとします。リソースが割り当てられた後にスローが発生しますが、beforehandleが入力されます!繰り返しますが、ファイナライザは実行されますが、handleはまだゼロであり、ハンドルをリークします!

このリークの発生を防ぐために、extremely巧妙なコードを記述する必要があります。さて、あなたの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はtrueifおよびifロックが実際に取得されました。同様の要件がある場合は、実際に書く必要があります:

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

AllocateResourceを_Monitor.Enter_のように巧みに書くと、AllocateResourceの内部で何が起こっても、handleifとonly if割り当てを解除する必要があります。

そのためのテクニックを説明することは、この答えの範囲を超えています。この要件がある場合は、専門家に相談してください。

67
Eric Lippert

@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ブロックに注意してください。

32
David Heffernan

このコード(元のサンプルに基づく):

_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 [〜#〜] (in Visual Studio 201 、ターゲティング 。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_で行われる最初の割り当ての後まで開始されません。一見すると、これはappearを実行して、保護されていないコードの最初の項目を割り当てます。ただし、結果はロケーション0に格納されていることに注意してください。2番目の割り当てが失敗すると、outer_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コードを見ると少し混乱します。何が起こっているのかを適切に理解する前に数分間じっと見つめなければなりませんでした。この。ただし、生成されたコードは攻撃不可能な真実です。

17
Tim Long

@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);
    }
}
7
wdosanjos