[C#] .NET에 좀비가 존재합니까?

.NET에서 잠금에 대해 팀원과 토론했습니다. 그는 저수준 및 고수준 프로그래밍 모두에서 광범위한 배경을 가진 정말 밝은 사람이지만, 저수준 프로그래밍에 대한 그의 경험은 내 것을 훨씬 능가합니다. 어쨌든 그는 시스템을 충돌시키는 “좀비 스레드”의 작은 가능성을 피하기 위해 가능한 한 과부하가 걸리는 중요한 시스템에서는 .NET 잠금을 피해야한다고 주장했다. 나는 일상적으로 잠금을 사용하고 “좀비 실”이 무엇인지 몰랐기 때문에 물었다. 내가 그의 설명에서 얻은 인상은 좀비 스레드가 종료되었지만 어떻게 든 여전히 일부 리소스를 보유하고있는 스레드라는 것입니다. 그가 좀비 스레드가 어떻게 시스템을 중단시킬 수 있는지에 대한 예는 스레드가 어떤 객체를 잠근 후에 어떤 절차를 시작한다는 것입니다. 그런 다음 잠금이 해제되기 전에 어느 시점에 종료됩니다. 이 상황에서는 시스템이 중단 될 가능성이 있습니다. 결국 해당 메소드를 실행하려고하면 잠긴 오브젝트를 사용하는 스레드가 종료 되었기 때문에 스레드가 리턴되지 않는 오브젝트에 대한 액세스를 모두 대기하게됩니다.

나는 이것의 요지가 있다고 생각하지만, 내가 기지에서 벗어나면 알려주십시오. 이 개념은 나에게 이해가되었다. 이것이 .NET에서 발생할 수있는 실제 시나리오라는 것을 완전히 확신하지 못했습니다. 이전에는 “좀비”에 대해 들어 본 적이 없지만 저수준에서 깊이 작업 한 프로그래머는 스레딩과 같은 컴퓨팅 기본 사항에 대해 더 깊이 이해하는 경향이 있음을 알고 있습니다. 나는 확실히 잠금의 가치를 보았고 많은 세계적인 프로그래머들이 잠금을 사용하는 것을 보았습니다. 나는 또한 그 lock(obj)진술이 실제로 구문 설탕 이라는 것을 알고 있기 때문에 이것을 스스로 평가하는 능력이 제한적 입니다.

bool lockWasTaken = false;
var temp = obj;
try { Monitor.Enter(temp, ref lockWasTaken); { body } }
finally { if (lockWasTaken) Monitor.Exit(temp); }

때문 Monitor.EnterMonitor.Exit표시됩니다 extern. .NET은 이러한 종류의 영향을 미칠 수있는 시스템 구성 요소에 대한 노출로부터 스레드를 보호하는 일종의 처리를 수행 할 수 있지만 순전히 추측 적이며 아마도 “좀비 스레드”에 대해 들어 본 적이 없다는 사실을 기반으로합니다. 전에. 따라서 여기에 대한 의견을 얻을 수 있기를 바랍니다.

  1. 여기서 설명한 것보다 “좀비 스레드”에 대한 명확한 정의가 있습니까?
  2. 좀비 스레드가 .NET에서 발생할 수 있습니까? (왜 왜 안돼?)
  3. 해당되는 경우 .NET에서 좀비 스레드를 작성하려면 어떻게해야합니까?
  4. 해당되는 경우 .NET에서 좀비 스레드 시나리오에 영향을주지 않으면 서 잠금을 어떻게 활용할 수 있습니까?

최신 정보

나는 2 년 전에이 질문을 조금했다. 오늘 이것은 일어났다 :

개체가 좀비 상태입니다.



답변

  • 여기서 설명한 것보다 “좀비 스레드”에 대한 명확한 정의가 있습니까?

나에게 아주 좋은 설명 인 것처럼 보입니다. 종료되어 (더 이상 자원을 해제 할 수는 없지만) 자원 (예 : 핸들)이 여전히 있고 (잠재적으로) 문제를 일으키는 스레드입니다.

  • 좀비 스레드가 .NET에서 발생할 수 있습니까? (왜 왜 안돼?)
  • 해당되는 경우 .NET에서 좀비 스레드를 작성하려면 어떻게해야합니까?

그들은 확실히, 내가 봐, 나는 하나를 만들었습니다!

[DllImport("kernel32.dll")]
private static extern void ExitThread(uint dwExitCode);

static void Main(string[] args)
{
    new Thread(Target).Start();
    Console.ReadLine();
}

private static void Target()
{
    using (var file = File.Open("test.txt", FileMode.OpenOrCreate))
    {
        ExitThread(0);
    }
}

이 프로그램은 Target파일을 여는 스레드 를 시작한 다음을 사용하여 즉시 종료됩니다 ExitThread. 결과로 나오는 좀비 스레드는 “test.txt”파일에 대한 핸들을 해제하지 않으므로 프로그램이 종료 될 때까지 파일이 열린 상태로 유지됩니다 (프로세스 탐색기 또는 이와 유사한 방법으로 확인할 수 있음). “test.txt”에 대한 핸들 GC.Collect은 호출 될 때까지 해제되지 않습니다. 핸들을 누설하는 좀비 스레드를 만드는 것보다 훨씬 어렵다는 것이 밝혀졌습니다)

  • 해당되는 경우 .NET에서 좀비 스레드 시나리오에 영향을주지 않으면 서 잠금을 어떻게 활용할 수 있습니까?

내가 방금 한 일을하지 마십시오!

코드가 자체적으로 올바르게 정리되는 한 ( 관리되지 않는 리소스로 작업하는 경우 Safe Handles 또는 동등한 클래스 사용 ), 이상하고 멋진 방법으로 스레드를 중단하지 않는 한 (가장 안전한 방법은 스레드를 죽이는 적이에 – 그들이 일반적으로 자신을 종료하자, 또는 뭔가 간 경우 예외 필요한 경우), 당신이 뭔가 좀비 스레드를 닮은이가는 유일한 방법 통해 매우 (예를 들어 뭔가 CLR에서 잘못) 잘못된.

실제로 좀비 스레드를 만드는 것은 실제로 놀랍게도 어렵습니다 (필자는 C 외부에서 호출하지 말라고 본질적으로 알려주는 함수로 P / 호출해야했습니다). 예를 들어 다음 코드는 실제로 좀비 스레드를 만들지 않습니다.

static void Main(string[] args)
{
    var thread = new Thread(Target);
    thread.Start();
    // Ugh, never call Abort...
    thread.Abort();
    Console.ReadLine();
}

private static void Target()
{
    // Ouch, open file which isn't closed...
    var file = File.Open("test.txt", FileMode.OpenOrCreate);
    while (true)
    {
        Thread.Sleep(1);
    }
    GC.KeepAlive(file);
}

꽤 끔찍한 실수를하더라도, “test.txt”에 대한 핸들 Abort은 호출 되 자마자 닫힙니다 ( file커버 아래에서 SafeFileHandle 을 사용 하여 파일 핸들을 래핑 하는 종료 자의 일부 )

C.Evenhuis 답변 의 잠금 예제는 스레드비정상적 이지 않은 방식으로 종료 될 때 리소스 (이 경우 잠금)를 해제하지 못하는 가장 쉬운 방법 일 수 있지만 lock대신 문을 사용하여 쉽게 해결할 수 있습니다. 릴리스를 finally블록 에 넣습니다 .

또한보십시오


답변

내 답변을 약간 정리했지만 참조를 위해 아래에 원래 답변을 남겼습니다.

좀비라는 용어에 대해 들어 본 것은 이번이 처음이므로 그 정의를 다음과 같이 가정하겠습니다.

모든 자원을 공개하지 않고 종료 된 스레드

따라서 그 정의가 주어지면 다른 언어 (C / C ++, java)와 마찬가지로 .NET에서 그렇게 할 수 있습니다.

그러나 나는 이것이 .NET에서 스레드 된 미션 크리티컬 코드를 작성하지 않는 좋은 이유라고 생각하지 않습니다. .NET에 대해 결정해야 할 다른 이유가있을 수 있지만 좀비 스레드가 어떻게 든 이해가되지 않기 때문에 .NET을 작성하는 것이 좋습니다. C / C ++에서 좀비 스레드가 가능합니다 (C에서 엉망이되는 것이 훨씬 쉽다고 주장합니다).

결론
사용할 언어를 결정하는 중이라면 성능, 팀 기술, 일정, 기존 앱과의 통합 등과 같은 큰 그림을 고려하는 것이 좋습니다. 물론 좀비 스레드는 고려해야 할 사항입니다. .NET에서 C와 같은 다른 언어와 비교할 때 실제로 실수하기가 너무 어렵 기 때문에 위에서 언급 한 것과 같은 다른 문제로 인해이 문제가 어두워 질 것이라고 생각합니다. 행운을 빕니다!

적절한 스레딩 코드를 작성하지 않으면 원래 답변
좀비 가 존재할 수 있습니다. C / C ++ 및 Java와 같은 다른 언어에서도 마찬가지입니다. 그러나 이것이 .NET에서 스레드 코드를 작성하지 않는 이유는 아닙니다.

다른 언어와 마찬가지로 무언가를 사용하기 전에 가격을 알아야합니다. 또한 잠재적 인 문제를 예측할 수 있도록 후드에서 발생하는 상황을 파악하는 데 도움이됩니다.

어떤 언어를 사용하든 미션 크리티컬 시스템을위한 안정적인 코드를 작성하는 것은 쉽지 않습니다. 그러나 .NET에서 올바르게 수행하는 것은 불가능하지 않습니다. 또한 AFAIK, .NET 스레딩은 C / C ++의 스레딩과 다르지 않으며 일부 .net 관련 구문 (예 : RWL 및 이벤트 클래스의 경량 버전)을 제외하고는 동일한 시스템 호출을 사용하거나 그로부터 작성됩니다.

좀비라는 말을 처음 들었을 때, 당신의 설명에 따르면, 동료는 아마 모든 리소스를 공개하지 않고 종료되는 스레드를 의미했을 것입니다. 교착 상태, 메모리 누수 또는 기타 나쁜 부작용이 발생할 수 있습니다. 이것은 분명히 바람직하지만, 이것 때문에 .NET을 지목하지 가능성 아마 너무 다른 언어로 가능하기 때문에 좋은 생각이 아니다. 나는 심지어 .NET (특히 RAII가없는 C)보다 C / C ++에서 엉망이 쉽지만 많은 중요한 앱이 C / C ++로 작성되었다고 주장하고 싶습니까? 그래서 그것은 실제로 당신의 개인적인 상황에 달려 있습니다. 응용 프로그램에서 모든 속도를 추출하고 가능한 한 베어 메탈에 가깝게 접근하려는 경우 .NET 최고의 솔루션이 아닙니다. 당신이 단단한 예산에 및 / 웹 서비스와 인터페이스 .NET 라이브러리 / 등 기존의 많이 할 경우, .NET이 있을 수 있습니다 좋은 선택.


답변

현재 내 답변의 대부분은 아래 의견에 의해 수정되었습니다. 의견의 정보가 독자에게 가치가 있기 때문에 평판 포인트가 필요하기 때문에 답변을 삭제하지 않습니다 .

Immortal Blue는 .NET 2.0 이상에서 finally블록이 스레드 중단에 영향을받지 않는다고 지적했습니다 . Andreas Niedermair의 의견에 따르면 실제 좀비 스레드는 아니지만 다음 예제에서는 스레드 중단으로 인해 문제가 발생할 수있는 방법을 보여줍니다.

class Program
{
    static readonly object _lock = new object();

    static void Main(string[] args)
    {
        Thread thread = new Thread(new ThreadStart(Zombie));
        thread.Start();
        Thread.Sleep(500);
        thread.Abort();

        Monitor.Enter(_lock);
        Console.WriteLine("Main entered");
        Console.ReadKey();
    }

    static void Zombie()
    {
        Monitor.Enter(_lock);
        Console.WriteLine("Zombie entered");
        Thread.Sleep(1000);
        Monitor.Exit(_lock);
        Console.WriteLine("Zombie exited");
    }
}

그러나 lock() { }블록을 사용할 finally때 a ThreadAbortException가 그런 식으로 발사 되면 여전히 실행됩니다 .

다음 정보는 .NET 1 및 .NET 1.1에만 유효합니다.

lock() { }블록 내부에서 다른 예외가 발생 ThreadAbortException하고 finally블록을 실행하려고 할 때 정확하게 도착 하면 잠금이 해제되지 않습니다. 언급했듯이 lock() { }블록은 다음과 같이 컴파일됩니다.

finally
{
    if (lockWasTaken)
        Monitor.Exit(temp);
}

Thread.Abort()생성 된 finally블록 내부에서 다른 스레드가 호출 되면 잠금이 해제되지 않을 수 있습니다.


답변

이것은 Zombie 스레드에 관한 것이 아니라 Effective C # 책에는 IDisposable 구현에 대한 섹션 (항목 17)이 있습니다.

책 자체를 읽는 것이 좋지만, IDisposable을 구현하거나 Desctructor를 포함하는 클래스가 있으면 리소스를 공개하는 것이 유일한 방법입니다. 여기서 다른 작업을 수행하면 객체가 가비지 수집되지 않지만 어떤 방식으로도 액세스 할 수 없게 될 가능성이 있습니다.

다음과 유사한 예를 제공합니다.

internal class Zombie
{
    private static readonly List<Zombie> _undead = new List<Zombie>();

    ~Zombie()
    {
        _undead.Add(this);
    }
}

이 객체의 소멸자가 호출되면 자체에 대한 참조가 전체 목록에 배치됩니다. 즉, 프로그램 수명 동안 살아 있고 메모리에 남아 있지만 액세스 할 수는 없습니다. 이는 리소스 (특히 관리되지 않는 리소스)가 완전히 해제되지 않아 모든 종류의 잠재적 인 문제가 발생할 수 있음을 의미 할 수 있습니다.

보다 완전한 예는 다음과 같습니다. foreach 루프에 도달 할 때마다 언데드 목록에 각각 이미지를 포함하는 150 개의 객체가 있지만 이미지가 GC로 지정되어 있고 사용하려고하면 예외가 발생합니다. 이 예제에서는 이미지를 저장하려고 시도하거나 높이 및 너비와 같은 치수를 보려고 할 때 이미지로 무언가를 시도 할 때 ArgumentException (매개 변수가 유효하지 않음)이 표시됩니다.

class Program
{
    static void Main(string[] args)
    {
        for (var i = 0; i < 150; i++)
        {
            CreateImage();
        }

        GC.Collect();

        //Something to do while the GC runs
        FindPrimeNumber(1000000);

        foreach (var zombie in Zombie.Undead)
        {
            //object is still accessable, image isn't
            zombie.Image.Save(@"C:\temp\x.png");
        }

        Console.ReadLine();
    }

    //Borrowed from here
    //http://stackoverflow.com/a/13001749/969613
    public static long FindPrimeNumber(int n)
    {
        int count = 0;
        long a = 2;
        while (count < n)
        {
            long b = 2;
            int prime = 1;// to check if found a prime
            while (b * b <= a)
            {
                if (a % b == 0)
                {
                    prime = 0;
                    break;
                }
                b++;
            }
            if (prime > 0)
                count++;
            a++;
        }
        return (--a);
    }

    private static void CreateImage()
    {
        var zombie = new Zombie(new Bitmap(@"C:\temp\a.png"));
        zombie.Image.Save(@"C:\temp\b.png");
    }
}

internal class Zombie
{
    public static readonly List<Zombie> Undead = new List<Zombie>();

    public Zombie(Image image)
    {
        Image = image;
    }

    public Image Image { get; private set; }

    ~Zombie()
    {
        Undead.Add(this);
    }
}

다시 말하지만, 당신이 좀비 스레드에 대해 특히 묻고 있다는 것을 알고 있지만 질문 제목은 .net의 좀비에 관한 것입니다.


답변

로드가 많은 중요한 시스템에서는 성능 향상으로 인해 잠금없는 코드를 작성하는 것이 좋습니다. LMAX 와 같은 것을 살펴보고 이것이 어떻게 큰 토론을 위해 “기계적 동정”을 활용하는지 살펴보십시오 . 좀비 스레드가 걱정 되십니까? 나는 그것이 다림질해야 할 버그 일뿐 아니라 사용하지 않을 충분한 이유가 아닌 가장자리 케이스라고 생각합니다 lock.

친구가 그냥 화려하고 이국적인 용어를 모호하게하는 그의 지식을 과시하는 것처럼 들립니다! Microsoft UK에서 성능 랩을 실행하는 동안 항상 .NET에서이 문제의 인스턴스를 보지 못했습니다.


답변

1. 여기서 설명한 것보다 “좀비 스레드”에 대한 명확한 정의가 있습니까?

“Zombie Threads”가 존재한다는 데 동의합니다. 쓰레드에서 발생하지 않고 완전히 죽지 않은 리소스가 남은 스레드를 나타내는 용어이므로 “좀비”라는 이름입니다. 이 추천에 대한 설명은 돈에 꽤 맞습니다!

2. 좀비 스레드가 .NET에서 발생할 수 있습니까? (왜 왜 안돼?)

그렇습니다. 참고로 실제로 Windows에서는 “좀비”라고합니다. MSDN은 죽은 프로세스 / 스레드에 “좀비”라는 단어를 사용합니다.

자주 일어나는 것은 또 다른 이야기이며, 스레드 잠금과 같은 코딩 기술과 관행에 달려 있으며 잠시 동안 해본 적이 있습니다. 나는 당신에게 일어나는 시나리오에 대해 걱정하지 않을 것입니다.

그리고 예, @KevinPanko가 주석에서 올바르게 언급했듯이 “Zombie Threads”는 Unix에서 왔기 때문에 XCode-ObjectiveC에서 사용되며 “NSZombie”라고하며 디버깅에 사용됩니다. 그것은 거의 같은 방식으로 작동합니다 … 유일한 차이점은 코드에서 잠재적 인 문제 일 수있는 “좀비 스레드”대신 디버깅을 위해 “좀비 객체”가된다는 것입니다.


답변

좀비 실을 쉽게 만들 수 있습니다.

var zombies = new List<Thread>();
while(true)
{
    var th = new Thread(()=>{});
    th.Start();
    zombies.Add(th);
}

스레드 핸들이 누출됩니다 (for Join()). 우리가 관리되는 세계에서 우려하는 한 또 다른 메모리 누수입니다.

이제 실제로 잠금을 유지하는 방식으로 실을 죽이는 것은 후면의 고통이지만 가능합니다. 다른 사람 ExitThread()이 일을 해요. 그가 찾은 것처럼 파일 핸들은 gc에 의해 정리되었지만 lock객체 주위는 그렇지 않습니다. 그러나 왜 그렇게하겠습니까?