it-swarm.com.ru

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

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

Обновление

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

Object is in a zombie state.

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

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

  • Могут ли потоки зомби появляться в .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", поэтому файл будет оставаться открытым до завершения программы (вы можете проверить это с помощью Process Explorer или аналогичного). Дескриптор "test.txt" не будет выпущен до тех пор, пока не будет вызван GC.Collect - получается, что это даже сложнее, чем я думал, создать поток зомби, который пропускает дескрипторы)

  • Если применимо, как я могу использовать блокировку, не рискуя сценарием потока зомби в .NET?

Не делай то, что я только что сделал!

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

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

Смотрите также

231
Justin

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

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

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

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

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

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

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

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

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

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

44
Jerahmeel

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

Бессмертный Синий отметил, что в .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 будет по-прежнему выполняться, когда ThreadAbortException запускается таким образом.

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

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

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

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

25
C.Evenhuis

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

Я рекомендую прочитать саму книгу, но суть ее в том, что если у вас есть класс, реализующий 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, и мне напомнили об этом, и я подумал, что другие могут найти это интересным!

22
JMK

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

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

20
James World

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

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

2. Могут ли темы зомби возникать в .NET? (Почему, почему нет?)

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

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

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

3
Steven Hernandez

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

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

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

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

0
Joshua