둘 이상의 리소스를 "사용"하면 리소스 누수가 발생할 수 있습니까?


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'd를 받게됩니다.
SLaks

3
using(... , ...)상관없이 블록을 사용하여 중첩으로 컴파일되는 경우 놀라지 않을 것이지만 확실하지는 않습니다.
Dan J

1
그건 나의 의도가 아니 었어. 전혀 사용하지 않더라도 usingGC는 결국 수집합니다.
SLaks

1
@zneak : 단일 finally블록으로 컴파일 되었 더라면 모든 리소스가 구성 될 때까지 블록에 들어오지 않았을 것입니다.
SLaks

2
@zneak : a 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, btw의 8.13입니다.
Ben Voigt 2014 년

11
@WeylandYutani : 당신은 무엇을 요구하고 있습니까?
SLaks

9
@WeylandYutani : 이것은 질의 응답 사이트입니다. 질문이 있으면 새 질문을 시작하십시오!
Eric Lippert 2014 년

5
@ user1306322 왜? 정말로 알고 싶다면 어떻게해야합니까?
Oxymoron 2014 년

2
@Oxymoron 그런 다음 조사 및 추측의 형태로 질문을 게시하기 전에 노력의 증거를 제공해야합니다. 개인적인 경험을 바탕으로 한 조언입니다.
user1306322

67

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


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

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


6
@gnat : 받아 들여진 대답. S는 무언가를 대표해야합니다. :-)
Eric Lippert 2014 년

12
@Joe : 물론 예제는 인위적 입니다. 나는 단지 그것을 고안했다 . 위험 수준 을 언급하지 않았기 때문에 위험은 과장 되지 않았습니다 . 오히려 나는이 패턴이 가능하다고 말했습니다 . 필드를 설정하는 것이 문제를 직접 해결한다고 믿는 사실은 내 요점을 정확하게 나타냅니다. 이런 종류의 문제에 대해 경험이없는 대다수의 프로그래머와 마찬가지로 여러분은이 문제를 해결할 능력이 없습니다. 실제로, 대부분의 사람들은이 인식하지 않는 것입니다 , 이는 문제가 내가 처음에이 답을 쓴 이유 .
Eric Lippert 2014 년

5
@Chris : 할당과 반환 사이, 그리고 반환과 할당 사이에 수행 된 작업이 없다고 가정합니다. 모든 Blah메서드 호출을 삭제 합니다. 이러한 지점 중 하나에서 ThreadAbortException이 발생하는 것을 막는 것은 무엇입니까?
Eric Lippert 2014 년

5
@Joe : 이것은 토론 사회가 아닙니다. 더 설득력있게 점수를 올리려는 것이 아닙니다 . 회의적이며 올바르게 해결하기 위해 전문가와의 상담이 필요한 까다로운 문제라는 내 말을 받아들이고 싶지 않다면 나와 동의하지 않아도됩니다.
Eric Lippert 2014 년

7
@GilesRoberts : 문제를 어떻게 해결합니까? 예외가 발생한다고 가정 한 후 호출 AllocateResource하지만 이전 에 할당 x. ThreadAbortException그 시점에서 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에 저장됩니다. 두 번째 할당이 실패하면 외부 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 코드는보기에 약간 혼란스럽고, 무슨 일이 일어나고 있는지 제대로 이해하기 전에 몇 분 동안 그것을 쳐다보아야했습니다. 이. 그러나 생성 된 코드는 공격 할 수없는 진실입니다.


@Peter Mortensen은 IL_0012와 IL_0017 사이의 IL 코드 청크를 제거하여 설명을 유효하지 않고 혼란스럽게 렌더링합니다. 이 코드는 내가 얻은 결과 의 축 어적 사본 으로 의도 되었으며 편집하면 무효화됩니다. 편집 내용을 검토하고 이것이 의도 한 것인지 확인해 주시겠습니까?
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
문제는 두 번째가 아닌 사용 목록에서 첫 번째 리소스의 폐기에 관한 것입니다. " font4 = new Font던지기를 하면 어떻게됩니까 ? 내가 이해하는 바에서 font3는 리소스를 누출하고 폐기되지 않을 것입니다."
wdosanjos
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.