Существуют ли зомби... в .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.Enter и Monitor.Exit отмечены extern. Представляется, что .NET делает какую-то обработку, которая защищает потоки от воздействия системных компонентов, которые могут иметь такое влияние, но это чисто умозрительное и, вероятно, только на основе того факта, что я никогда не слышал о "потоках зомби", до. Итак, я надеюсь, что смогу получить некоторые отзывы об этом здесь:

  • Есть ли более четкое определение "зомби-потока", чем то, что я здесь объяснил?
  • Могут ли зомби-потоки встречаться на .NET? (Почему/почему?)
  • Если применимо, как я могу заставить создание потока зомби в .NET?
  • Если применимо, как я могу использовать блокировку, не рискуя сценарием потока зомби в .NET?

Update

Я задал этот вопрос чуть более двух лет назад. Сегодня это произошло:

Объект находится в состоянии зомби.

Ответ 1

  • Есть ли более четкое определение "зомби-потока", чем то, что я здесь объяснил?

Мне кажется очень хорошим объяснением - поток, который прекратил (и, следовательно, больше не может освобождать какие-либо ресурсы), но ресурсы которого (например, дескрипторы) все еще существуют и (потенциально) вызывают проблемы.

  • Могут ли потоки зомби встречаться на .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 или эквивалентные классы при работе с неуправляемыми ресурсами) и до тех пор, пока вы не уходите с пути, чтобы убивать потоки странными и прекрасными способами (самый безопасный способ - просто никогда не убивать нити - пусть они прекращают себя нормально или через исключения, если это необходимо), единственный способ, которым у вас будет что-то похожий на зомби-поток, если что-то пошло не так (very) (например, что-то не так в CLR).

На самом деле его на самом деле удивительно сложно создать зомби-поток (мне пришлось P/Invoke в функцию, которая esentially говорит вам в документации, чтобы не вызывать ее за пределами C). Например, следующий (ужасный) код фактически не создает поток зомби.

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 answer, вероятно, самый простой способ не освободить ресурс (блокировка в этом случае), когда поток завершается в но это легко установить либо с помощью инструкции lock, либо для размещения релиза в блоке finally.

См. также

Ответ 2

Я немного починил свой ответ, но оставил оригинал ниже для справки

Впервые я слышал о терминах зомби, поэтому я буду считать, что его определение:

Поток, который завершился без освобождения всех его ресурсов

Итак, учитывая это определение, да, вы можете сделать это в .NET, как и в других языках (C/С++, java).

Однако, я не думаю, что это хорошая причина не писать потоковый критически важный код в .NET. Могут быть и другие причины для принятия решения против .NET, но списание .NET просто потому, что у вас могут быть потоки зомби, для меня это не имеет смысла. Потоки Zombie возможны в C/С++ (я бы даже утверждать, что его намного проще испортить на C), и множество критических приложений с потоком находятся в C/С++ (торговля большими объемами, базы данных и т.д.).

Заключение Если вы решаете использовать язык, я предлагаю вам принять во внимание большую картину: производительность, навыки команды, график, интеграцию с существующими приложениями и т.д. Конечно, потоки зомби - это то, о чем вы должны думать, но поскольку его настолько сложно сделать эту ошибку в .NET по сравнению с другими языками, как C, я думаю, что эта проблема будет омрачена другими вещами, такими как упомянутые выше. Удачи!

Оригинальный ответ Зомби могут существовать, если вы не записываете правильный код потока. То же самое верно для других языков, таких как C/С++ и Java. Но это не повод не писать threaded-код в .NET.

И так же, как и на любом другом языке, знайте цену, прежде чем использовать что-то. Это также помогает узнать, что происходит под капотом, чтобы вы могли предвидеть любые потенциальные проблемы.

Надежный код для критически важных систем нелегко записать на любом языке, на котором вы находитесь. Но я уверен, что не корректно делать это в .NET. Также AFAIK, потоковая передача .NET не отличается от потоковой обработки в C/С++, она использует (или построена) одни и те же системные вызовы, за исключением некоторых .net-специфичных конструкций (например, облегченных версий RWL и классов событий).

Впервые я слышал о терминах зомби, но, основываясь на вашем описании, ваш коллега, вероятно, имел в виду поток, который завершался без освобождения всех ресурсов. Это может потенциально вызвать тупик, утечку памяти или какой-либо другой плохой побочный эффект. Это явно нежелательно, но выделение .NET из-за этой возможности, вероятно, не является хорошей идеей, так как это возможно и на других языках. Я бы даже утверждал, что его легче испортить в C/С++, чем в .NET(особенно на C, где у вас нет RAII), но много критических приложений написано на C/С++ правильно? Так что это действительно зависит от ваших индивидуальных обстоятельств. Если вы хотите извлечь каждую унцию скорости из своего приложения и хотите как можно ближе подойти к голым металлам, то .NET может оказаться не лучшим решением. Если вы находитесь в ограниченном бюджете и много взаимодействуете с веб-службами/существующими библиотеками .net/etc, то .NET может быть хорошим выбором.

Ответ 3

В настоящий момент большая часть моего ответа была исправлена ​​комментариями ниже. Я не удалю ответ , потому что мне нужны точки репутации, потому что информация в комментариях может быть полезной для читателей.

Immortal Blue отметил, что в .NET 2.0 и up 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 все равно будет выполняться при запуске ThreadAbortException.

Следующая информация, как оказалось, действительна только для .NET 1 и .NET 1.1:

Если внутри блока lock() { } возникает другое исключение, а ThreadAbortException поступает точно, когда блок finally должен быть запущен, блокировка не будет выпущена. Как вы упомянули, блок lock() { } скомпилирован как:

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

Если другой поток вызывает Thread.Abort() внутри сгенерированного блока finally, блокировка может не быть выпущена.

Ответ 4

Речь идет не о потоках Zombie, но в книге Effective С# есть раздел о реализации IDisposable (пункт 17), в котором рассказывается о объектах Zombie, которые, как я думал, вам интересны.

Я рекомендую прочитать саму книгу, но суть в том, что если у вас есть класс, реализующий IDisposable или содержащий Desctructor, единственное, что вы должны делать, это освобождение ресурсов. Если вы делаете другие вещи здесь, то есть шанс, что объект не будет собираться мусором, но также не будет доступен каким-либо образом.

Здесь приведен пример, похожий на ниже:

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

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

Когда деструктор на этом объекте вызывается, ссылка на себя помещается в глобальный список, то есть он остается живым и в памяти для жизни программы, но недоступен. Это может означать, что ресурсы (особенно неуправляемые ресурсы) могут быть не полностью выпущены, что может вызвать всевозможные потенциальные проблемы.

Ниже приведен более полный пример. К моменту достижения цикла foreach у вас есть 150 объектов в списке Undead, каждый из которых содержит изображение, но изображение было GC'd, и вы получаете исключение, если пытаетесь его использовать. В этом примере я получаю 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, и мне напомнили об этом и подумали, что другие могут найти это интересным!

Ответ 5

В критических системах с большой нагрузкой писать код без блокировки лучше в первую очередь из-за улучшений производительности. Посмотрите на такие вещи, как LMAX и как он использует "механическую симпатию" для больших дискуссий об этом. Беспокоитесь о потоках зомби? Я думаю, что краевой кейс, который просто ошибка, чтобы быть сглаженным, и не достаточно хорошая причина не использовать lock.

Похоже, что ваш друг просто притворяется и щеголяет своим знанием неясной экзотической терминологии мне! В течение всего времени, когда я запускал лаборатории производительности в Microsoft UK, я никогда не сталкивался с экземпляром этой проблемы в .NET.

Ответ 6

1. Есть ли более четкое определение "зомби-потока", чем то, что я здесь объяснил?

Я согласен с тем, что существуют "Zombie Threads", это термин, чтобы ссылаться на то, что происходит с Threads, которые остаются с ресурсами, которые они не отпускают и все же не полностью умирают, отсюда и название "зомби", "так что ваше объяснение этого реферала довольно верно на деньги!

2. Каменные потоки зомби встречаются на .NET? (Почему/почему?)

Да, они могут произойти. Это ссылка, на самом деле называемая Windows как "зомби": MSDN использует Word "Zombie" для мертвых процессов/потоков

Часто случается, что это другая история, и зависит от ваших методов и методов кодирования, так как для вас, вроде Thread Locking, и делал это какое-то время, я бы даже не беспокоился о том, что этот сценарий происходит с вами.

И да, как правильно упомянул @KevinPanko в комментариях, "Zombie Threads" поступает из Unix, поэтому они используются в XCode-ObjectiveC и называются "NSZombie" и используются для отладки. Он ведет себя практически так же... Единственное отличие - это тот объект, который должен был умереть, превратился в "ZombieObject" для отладки вместо "Zombie Thread", который может быть потенциальной проблемой в вашем коде.

Ответ 7

Я могу создавать потоки зомби достаточно легко.

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

Это утечка ручек потока (для Join()). Это просто еще одна утечка памяти, насколько мы обеспокоены в управляемом мире.

Теперь, убивая нить таким образом, что она фактически удерживает блокировки, является боль сзади, но возможно. Другой парень ExitThread() выполняет эту работу. Как он нашел, дескриптор файла был очищен gc, но lock вокруг объекта не будет. Но зачем вам это делать?