[c#] 둘 이상의 리소스를 “사용”하면 리소스 누수가 발생할 수 있습니까?

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(... , ...)중첩 사용을 위해 아예 피해야 함을 의미합니까 ?


답변

아니.

컴파일러는 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

답변

업데이트 : 나는 여기 에서 찾을 수있는 기사의 기초로이 질문을 사용했다 ; 이 문제에 대한 추가 논의는 그것을 참조하십시오. 좋은 질문에 감사드립니다!


Schabse의 답변 은 물론 정확하고 질문에 대한 답변 이지만 질문하지 않은 중요한 변형이 있습니다.

하면 어떻게됩니까 font4 = new Font()발생 관리되지 않는 리소스가 생성자가 아니라 할당 된 에서의 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();
    }
}

확인. Whatever던진다 고 가정하자 . 그런 다음 finally블록이 실행되고 리소스가 할당 해제됩니다. 문제 없어요.

Blah1()던진다 고 가정하자 . 그런 다음 리소스가 할당되기 전에 throw가 발생합니다. 객체가 할당되었지만 ctor는 반환 foo되지 않으므로 채워 try지지 않습니다 finally. 개체 참조가 분리되었습니다. 결국 GC는이를 발견하여 종료 자 대기열에 넣습니다. handle여전히 0이므로 종료자는 아무 작업도 수행하지 않습니다. 종료자는 생성자가 완료되지 않은 객체가 종료되는 상황에서 견고해야합니다 . 당신은있다 필요한 이 강한 파이널 라이저를 작성. 이것이 최종 결정자를 전문가에게 맡기고 직접 시도하지 않아야하는 또 다른 이유입니다.

Blah3()던진다 고 가정하자 . 리소스가 할당 된 후에 throw가 발생합니다. 그러나 다시, foo채워지지 않고, 우리는 절대로 입력하지 않으며 finally, 객체는 종료 자 스레드에 의해 정리됩니다. 이번에는 핸들이 0이 아니므로 종료자가이를 정리합니다. 다시 말하지만, 종료자는 생성자가 성공한 적이없는 객체에서 실행되지만 종료자는 어쨌든 실행됩니다. 이번에는해야 할 일이 있었기 때문에 분명히해야합니다.

이제 던졌다 고 가정 Blah2()합니다. 던지는 리소스가 할당 된 후, 채워 지기 전에 발생합니다 handle! 다시 말하지만, 파이널 라이저는 실행되지만 지금 handle은 여전히 ​​0이고 핸들을 누출합니다!

이 누출이 발생하지 않도록하려면 매우 영리한 코드 를 작성해야합니다 . 자, 당신의 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.EnterAllocateResourcehandle

그렇게하는 기술을 설명하는 것은이 답변의 범위를 벗어납니다. 이 요구 사항이 있으면 전문가에게 문의하십시오.


답변

@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 블록에 유의하십시오.


답변

이 코드 (원래 샘플 기반) :

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기본적으로 중첩 된 usings 로 확장 되는지 확인합니다 . CIL 코드는보기에 약간 혼란스럽고, 무슨 일이 일어나고 있는지 제대로 이해하기 전에 몇 분 동안 그것을 쳐다보아야했습니다. 이. 그러나 생성 된 코드는 공격 할 수없는 진실입니다.


답변

다음은 @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);
    }
}


답변