문제

요약: C#/. Net은 쓰레기를 수집해야합니다. C#에는 자원을 청소하는 데 사용되는 소멸자가 있습니다. 객체 A가 쓰레기가 변수 구성원 중 하나를 복제하려고하는 것과 같은 선을 수집했을 때 어떻게됩니까? 분명히, 다중 프로세서에서 때로는 쓰레기 수집가가 승리합니다.

문제

오늘 C#에 대한 교육 세션에서 교사는 멀티 프로세서에서 실행될 때만 버그가 포함 된 코드를 보여주었습니다.

나는 호출 된 메소드에서 돌아 오기 전에 c# 클래스 객체의 최종화기를 호출하여 컴파일러 또는 JIT가 나사로 고정된다고 말할 것입니다.

Visual C ++ 2005 문서에 제공된 전체 코드는 매우 큰 질문을 피하기 위해 "답변"으로 게시되지만 필수는 다음과 같습니다.

다음 클래스에는 "해시"속성이있어 내부 배열의 복제 된 사본을 반환합니다. At Is Construction, 배열의 첫 번째 항목의 값은 2입니다. 소멸자에서 그 값은 0으로 설정됩니다.

요점은 : "example"의 "해시"속성을 얻으려고한다면, 첫 번째 항목은 2 인 배열의 깨끗한 사본을 얻을 수 있습니다. 쓰레기 수집/마무리) :

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

그러나이 클래스를 사용하는 코드는 스레드 내부에서 흔들리고 있으며, 물론 테스트를 위해 앱은 크게 멀티 스레드되었습니다.

public static void Main(string[] args)
{
    Thread t = new Thread(new ThreadStart(ThreadProc));
    t.Start();
    t.Join();
}

private static void ThreadProc()
{
    // running is a boolean which is always true until
    // the user press ENTER
    while (running) DoWork();
}

Dowork 정적 방법은 문제가 발생하는 코드입니다.

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2)
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
}

Dowork의 1,000,000 개를 1,000,000 건의 발굴 한 번, 쓰레기 수집가는 마법을 수행하고 "EX"를 되 찾으려고 시도합니다. 이는 기능의 리밍 코드에서 더 이상 참조되지 않으며 이번에는 "해시"보다 빠릅니다. 방법을 얻으십시오. 따라서 결국 우리가 가진 것은 올바른 배열을 갖지 않고 제로 에드 바이트 배열의 복제품입니다 (첫 번째 항목은 2에서 첫 번째 항목으로).

내 생각에 코드가 인화되어 있는데, 이는 Dowork 함수의 [1]이 다음과 같은 것들로 표시된 선을 본질적으로 대체하는 것입니다.

    // Supposed inlined processing
    byte[] res2 = ex.Hash2;
    // note that after this line, "ex" could be garbage collected,
    // but not res2
    byte[] res = (byte[])res2.Clone();

Hash2가 간단한 액세서라고 가정하면 다음과 같이 코딩 된 간단한 액세서입니다.

// Hash2 code:
public byte[] Hash2 { get { return (byte[])hashValue; } }

따라서 문제는 다음과 같습니다. 이것은 C#/. Net에서 그런 식으로 작동해야합니까, 아니면 JIT의 컴파일러의 버그로 간주 될 수 있습니까?

편집하다

설명은 Chris Brumme와 Chris Lyons의 블로그를 참조하십시오.

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/2004/09/21/232445.aspx

모든 사람의 대답은 흥미로 웠지만 다른 하나보다 더 잘 선택할 수 없었습니다. 그래서 나는 당신에게 모두 +1을 주었다 ...

죄송합니다

:-)

편집 2

동일한 조건에서 동일한 코드를 사용하더라도 Linux/Ubuntu/Mono에서 문제를 재현 할 수 없었습니다 (동시에 실행중인 다중 실행 파일, 릴리스 모드 등).

도움이 되었습니까?

해결책

단순히 코드의 버그입니다. Finalizers는 관리되는 객체에 액세스해서는 안됩니다.

최종화기를 구현하는 유일한 이유는 관리되지 않는 리소스를 릴리스하는 것입니다. 이 경우 신중하게 구현해야합니다 표준 idisposable 패턴.

이 패턴을 사용하면 보호 된 방법 "Protected Dispose (Bool Disposing)"를 구현합니다. 이 방법이 Finalizer에서 호출되면 관리되지 않은 리소스를 정리하지만 관리 자원을 정리하려고 시도하지는 않습니다.

예에서는 관리되지 않는 리소스가 없으므로 최종화기를 구현해서는 안됩니다.

다른 팁

당신이보고있는 것은 완벽하게 자연 스럽습니다.

바이트 배열을 소유 한 객체에 대한 참조를 유지하지 않으므로 바이트 어레이가 아닌 객체가 실제로 쓰레기 수집기가 수집 할 수있는 무료입니다.

쓰레기 수집가는 실제로 그렇게 공격적 일 수 있습니다.

따라서 내부 데이터 구조에 대한 참조를 반환하는 객체의 메소드를 호출하고 객체의 최종화기가 해당 데이터 구조를 엉망으로 만들면 객체에 대한 실시간 참조를 유지해야합니다.

쓰레기 수집가는 전 변수가 그 방법에 더 이상 사용되지 않는다는 것을 알 수 있으므로, 알다시피, 올바른 상황 (예 : 타이밍 및 필요)에서 쓰레기를 수집 할 수 있습니다.

이를 수행하는 올바른 방법은 ex에서 gc.keepalive를 호출하는 것입니다.이 코드 줄을 메소드의 맨 아래에 추가하면 모든 것이 좋습니다.

GC.KeepAlive(ex);

나는이 책을 읽음 으로써이 공격적인 행동에 대해 배웠다 .NET 프레임 워크 프로그래밍을 적용했습니다 Jeffrey Richter.

이것은 작업 스레드와 GC 스레드 사이의 레이스 조건처럼 보입니다. 피하기 위해 두 가지 옵션이 있다고 생각합니다.

(1) ex.hash [0]를 RES 대신 사용하도록 IF 문을 변경하여 EX는 조기에 GC를 가질 수 없거나

(2) 해시 호출 기간 동안 Ex를 잠그십시오.

그것은 꽤 끔찍한 예입니다. 교사의 요점은 Multicore 시스템에서만 나타나는 JIT 컴파일러에 버그가있을 수 있거나 이런 종류의 코딩이 쓰레기 수집으로 미묘한 경주 조건을 가질 수 있다는 버그가있을 수 있다는 점이었습니다.

나는 당신이보고있는 것이라고 생각합니다 합리적인 행동이 여러 실에서 실행되고 있다는 사실로 인해 행동. 이것이 GC.keepalive () 메소드의 이유이며,이 경우 객체가 여전히 사용되고 있으며 정리 후보가 아님을 GC에 알려야합니다.

"전체 코드"응답에서 Dowork 함수를 살펴보면 문제는이 코드 라인 직후에 다음과 같습니다.

byte[] res = ex.Hash;

함수는 더 이상 참조를하지 않습니다 전- 객체, 따라서 그 시점에서 쓰레기 수집을받을 자격이됩니다. gc.keepalive에 호출을 추가하면 이런 일이 발생하지 않습니다.

예, 이것은 an입니다 문제 그건 가지고 있습니다 전에 오세요.

이 일이 일어나기 위해 릴리스를 실행해야한다는 점에서 훨씬 더 재미 있고, 당신은 머리를 쓰다듬어 '허가가 될 수 있습니까?'

Chris Brumme의 블로그의 흥미로운 의견

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx

class C {<br>
   IntPtr _handle;
   Static void OperateOnHandle(IntPtr h) { ... }
   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();
         aC.m();
         ...  // most guess here
      } else {
         ...
      }
   }
}

따라서 위의 코드에서 'AC'가 얼마나 오래 살 수 있는지는 말할 수 없습니다. JIT는 다른.work ()가 완료 될 때까지 참조를보고 할 수 있습니다. 다른 방법으로 다른 방법으로 내려갈 수 있고 AC를 더 오래보고 할 수 있습니다. "ac = null"을 추가하더라도 사용 후 JIT는이 과제를 데드 코드로 간주하여 제거 할 수 있습니다. JIT가 참조보고를 중단 할 때에 관계없이 GC는 한동안 수집하지 않을 수 있습니다.

AC를 수집 할 수있는 가장 초기의 시점에 대해 걱정하는 것이 더 흥미 롭습니다. 당신이 대부분의 사람들과 마찬가지로, 가장 빨리 AC가 수집 자격이 될 수 있다고 생각할 것입니다. 실제로, 엘은 IL에 존재하지 않습니다. 그들은 당신과 당신의 언어 컴파일러 사이의 구문 계약입니다. Other.work ()는 AC.M ()에 호출을 시작하자마자 AC보고를 중지 할 수 있습니다.

Ex.Hash Call 이후에 Finalizer가 귀하의 DO 작업 방법에서 호출되는 것은 완벽하게 Nornal입니다. CLR은 EX 인스턴스가 더 이상 필요하지 않다는 것을 알고 있습니다 ...

이제 인스턴스를 생생하게 유지하려면 다음을 수행하십시오.

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2) // NOTE
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
  GC.KeepAlive(ex); // keep our instance alive in case we need it.. uh.. we don't
}

gc. 주인공은 ...

경고 : Dowork 방법이 관리되는 C ++ 방법 인 경우 예제는 완벽하게 유효합니다 ... ~하다 다른 스레드 내에서 파괴자를 호출하지 않으려면 관리되는 인스턴스를 수동으로 수동으로 유지해야합니다. 즉. 당신은 마무리 될 때 관리되지 않은 메모리 덩어리를 삭제하려는 관리되는 객체에 대한 참조를 전달하며,이 방법은 동일한 블로브를 사용하고 있습니다. 인스턴스를 살아 있지 않으면 GC와 메소드 스레드 사이에 레이스 조건이있을 것입니다.

그리고 이것은 끝날 것입니다. 그리고 힙 부패를 관리했습니다 ...

전체 코드

시각적 C ++ 2008 .CS 파일에서 복사/붙여 넣은 전체 코드 아래에 있습니다. 내가 지금 Linux에 있고 Mono 컴파일러 나 그 사용에 대한 지식이 없으면 지금 테스트를 수행 할 수있는 방법이 없습니다. 그럼에도 불구하고 몇 시간 전에이 코드가 작동하고 버그를 보았습니다.

using System;
using System.Threading;

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }
    public byte[] Hash2 { get { return (byte[])hashValue; } }

    public int returnNothing() { return 25; }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

public class Test
{
    private static int totalCount = 0;
    private static int finalizerFirstCount = 0;

    // This variable controls the thread that runs the demo.
    private static bool running = true;

    // In order to demonstrate the finalizer running first, the
    // DoWork method must create an Example object and invoke its
    // Hash property. If there are no other calls to members of
    // the Example object in DoWork, garbage collection reclaims
    // the Example object aggressively. Sometimes this means that
    // the finalizer runs before the call to the Hash property
    // completes. 

    private static void DoWork()
    {
        totalCount++;

        // Create an Example object and save the value of the 
        // Hash property. There are no more calls to members of 
        // the object in the DoWork method, so it is available
        // for aggressive garbage collection.

        Example ex = new Example();

        // Normal processing
        byte[] res = ex.Hash;

        // Supposed inlined processing
        //byte[] res2 = ex.Hash2;
        //byte[] res = (byte[])res2.Clone();

        // successful try to keep reference alive
        //ex.returnNothing();

        // Failed try to keep reference alive
        //ex = null;

        // If the finalizer runs before the call to the Hash 
        // property completes, the hashValue array might be
        // cleared before the property value is read. The 
        // following test detects that.

        if (res[0] != 2)
        {
            finalizerFirstCount++;
            Console.WriteLine("The finalizer ran first at {0} iterations.", totalCount);
        }

        //GC.KeepAlive(ex);
    }

    public static void Main(string[] args)
    {
        Console.WriteLine("Test:");

        // Create a thread to run the test.
        Thread t = new Thread(new ThreadStart(ThreadProc));
        t.Start();

        // The thread runs until Enter is pressed.
        Console.WriteLine("Press Enter to stop the program.");
        Console.ReadLine();

        running = false;

        // Wait for the thread to end.
        t.Join();

        Console.WriteLine("{0} iterations total; the finalizer ran first {1} times.", totalCount, finalizerFirstCount);
    }

    private static void ThreadProc()
    {
        while (running) DoWork();
    }
}

관심있는 사람들을 위해 이메일을 통해 ZIPPER 프로젝트를 보낼 수 있습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top