複数のリソースで「使用」すると、リソースリークが発生する可能性がありますか?


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年

回答:


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:これは質疑応答サイトです。質問がある場合は、新しい質問を開始してください!
Eric Lippert

5
@ user1306322なぜですか?私が本当に知りたい場合はどうなりますか?
Oxymoron、2014年

2
@Oxymoronは、質問をリサーチや推測の形式で投稿する前に、努力の証拠を提供する必要があります。個人的な経験に基づいたアドバイスです。
user1306322 2014年

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

今私たちは持っています

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はまだゼロなので、ファイナライザは何もしません。 ファイナライザは、コンストラクタが完了していないファイナライズ中のオブジェクトに対してロバストである必要があることに注意してください。あなたはされている必要この強力なファイナライザを作成するます。これは、ファイナライザの記述を専門家に任せ、自分でやろうとしないもう1つの理由です。

と思います 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ように非常に慎重に書かれている例外がスローされているものに関係なくはlockEnteredtrueに設定されている場合にのみ場合ロックは、ロックが実際に取得されたされています。同様の要件がある場合は、実際に書く必要があります。

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

書き込みAllocateResource巧みのようなMonitor.Enterので、内部に何が起こるかに関係なくがあることAllocateResourcehandleで満たされています割り当てを解除する必要がある場合のみます。

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


6
@gnat:受け入れられた答え。そのSは何かを表す必要があります。:-)
Eric Lippert

12
@Joe:もちろん例を考案します私はそれを考案しました。私はリスクのレベルが何であるかを述べていないので、リスクは誇張されていません。むしろ、私はこのパターンが可能であると述べました。フィールドを設定することで問題が直接解決されると信じているという事実は、私の指摘を正確に示しています。この種の問題を経験したことがない大多数のプログラマーと同様に、あなたはこの問題を解決する能力がありません。実際、ほとんどの人は問題あることさえ認識していません。そのため、私が最初にこの回答を書きました
Eric Lippert、2014年

5
@Chris:割り当てと戻りの間、および戻りと割り当ての間の作業がゼロであるとします。これらのBlahメソッド呼び出しはすべて削除します。ThreadAbortExceptionがこれらのポイントのいずれかで発生するのを阻止するものは何ですか?
Eric Lippert、2014年

5
@ジョー:これは議論の多い社会ではありません。私はもっ​​と説得力があることによってポイントを獲得することを望んでいません。あなたが懐疑的で、これを専門家と相談して正しく解決する必要があるトリッキーな問題だと私の意見を表明したくない場合は、私に同意することを歓迎します。
Eric Lippert、2014年

7
@GilesRoberts:どのようにして問題を解決しますか?例外が発生したと仮定した後に呼び出しAllocateResourceが、に割り当てxThreadAbortExceptionその時点でA が発生する可能性があります。ここの誰もが私のポイントを欠いているようです。それは、リソースの作成とそれへの参照の変数への割り当てはアトミック操作ではないということです。私が特定した問題を解決するには、それをアトミック操作にする必要があります。
Eric Lippert、2014年

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に格納されていることに注意してください。2番目の割り当てが失敗すると、外側の 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の間)が削除され、説明が無効で混乱を招きます。そのコードは、私が得た結果の逐語的なコピーであることを目的としており、編集するとそれが無効になります。編集内容を確認して、意図したとおりであることを確認してください。
Tim Long、

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
それはそれを証明していません。Dispose:t2はどこにありますか?:)
Piotr Perak 2014年

1
問題は、2番目ではなく、使用リストの最初のリソースの破棄についてです。 font4 = new Fontスローするとどうなりますか?font3はリソースをリークし、破棄されないことを理解しています。」
wdosanjos 2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.