Финализатор был запущен, когда его объект все еще использовался

StackOverflow https://stackoverflow.com/questions/134653

Вопрос

Краткие сведения: Предполагается, что C # / .NET предназначен для сбора мусора.В C # есть деструктор, используемый для очистки ресурсов.Что происходит, когда объект A является мусором, собранным в той же строке, в которой я пытаюсь клонировать один из его переменных-членов?По-видимому, в многопроцессорных системах иногда выигрывает сборщик мусора...

В чем проблема

Сегодня, на тренинге по C #, преподаватель показал нам некоторый код, который содержал ошибку только при запуске на мультипроцессорах.

Я подытожу, чтобы сказать, что иногда компилятор или JIT облажаются, вызывая финализатор объекта класса C # перед возвратом из вызванного им метода.

Полный код, приведенный в документации Visual C ++ 2005, будет опубликован в качестве "ответа", чтобы избежать возникновения очень-очень больших вопросов, но основные приведены ниже:

Следующий класс имеет свойство "Hash", которое вернет клонированную копию внутреннего массива.При построении is первый элемент массива имеет значение 2.В деструкторе его значение устанавливается равным нулю.

Дело в том, что:Если вы попытаетесь получить свойство "Hash" для "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
    }
}

По-видимому, после каждых 1 000 000 исключений DoWork сборщик мусора творит свое волшебство и пытается вернуть "ex", поскольку на него больше нет ссылок в коде восстановления функции, и на этот раз это быстрее, чем метод получения "Hash".Итак, что мы имеем в итоге, это клон массива с нулевым значением байта, вместо того, чтобы иметь правильный (с 1-м элементом в 2).

Я предполагаю, что есть вставка кода, которая по существу заменяет строку, отмеченную [1] в функции DoWork, чем-то вроде:

    // 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?

Редактировать

Объяснение можно найти в блогах Криса Брумма и Криса Лайонса.

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, несмотря на использование одного и того же кода в одних и тех же условиях (одновременный запуск нескольких одинаковых исполняемых файлов, режим выпуска и т.д.)

Это было полезно?

Решение

Это просто ошибка в вашем коде:завершители не должны получать доступ к управляемым объектам.

Единственная причина для внедрения финализатора - освободить неуправляемые ресурсы.И в этом случае вам следует тщательно внедрить стандартный шаблон IDisposable.

С помощью этого шаблона вы реализуете защищенный метод "protected Dispose(bool disposing)".Когда этот метод вызывается из финализатора, он очищает неуправляемые ресурсы, но не пытается очистить управляемые ресурсы.

В вашем примере у вас нет никаких неуправляемых ресурсов, поэтому не следует внедрять финализатор.

Другие советы

То, что вы видите, совершенно естественно.

Вы не сохраняете ссылку на объект, которому принадлежит массив байтов, так что этот объект (а не массив байтов) фактически свободен для сбора сборщиком мусора.

Сборщик мусора действительно может быть настолько агрессивным.

Поэтому, если вы вызываете метод для своего объекта, который возвращает ссылку на внутреннюю структуру данных, а финализатор для вашего объекта портит эту структуру данных, вам также необходимо сохранить текущую ссылку на объект.

Сборщик мусора видит, что переменная ex больше не используется в этом методе, поэтому он может, и, как вы заметили, будет собирать мусор при правильных обстоятельствах (т.е.сроки и необходимость).

Правильный способ сделать это - вызвать GC.KeepAlive в ex, поэтому добавьте эту строку кода в нижнюю часть вашего метода, и все должно быть хорошо:

GC.KeepAlive(ex);

Я узнал об этом агрессивном поведении, прочитав книгу Прикладное программирование на платформе .NET Framework автор: Джеффри Рихтер.

это похоже на состояние гонки между вашим рабочим потоком и потоками GC;чтобы избежать этого, я думаю, есть два варианта:

(1) измените свой оператор if, чтобы использовать ex.Hash[0] вместо res, чтобы ex не мог быть GC'd преждевременно, или

(2) заблокируйте ex на время вызова Hash

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

Я думаю, то, что вы видите, это разумный поведение связано с тем, что все выполняется в нескольких потоках.Это причина для метода GC.KeepAlive(), который следует использовать в этом случае, чтобы сообщить GC, что объект все еще используется и что он не является кандидатом на очистку.

Глядя на функцию DoWork в вашем ответе "полный код", проблема заключается в том, что сразу после этой строки кода:

byte[] res = ex.Hash;

функция больше не делает никаких ссылок на бывший объект, поэтому в этот момент он становится пригодным для сборки мусора.Добавление вызова в GC.KeepAlive предотвратило бы это.

Да, это проблема это имеет поднимись раньше.

Это еще веселее в том смысле, что вам нужно запустить release, чтобы это произошло, и в итоге вы ломаете голову над тем, "хм, как это может быть null?".

Интересный комментарий из блога Криса Брумма

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 может сообщать о ссылке до тех пор, пока функция Other.work() не завершит работу.Это могло бы встроить Other.work() в какой-нибудь другой метод и сообщать об aC еще дольше.Даже если вы добавите “aC = null;” после вашего использования, JIT может считать это назначение мертвым кодом и исключить его.Независимо от того, когда JIT перестанет сообщать ссылку, GC может не успеть собрать ее в течение некоторого времени.

Гораздо интереснее беспокоиться о самой ранней точке, в которой может быть собран aC.Если вы похожи на большинство людей, вы догадаетесь, что самый быстрый AC, который получит право на сбор, находится в закрывающей фигурной скобке предложения "if” в Other.work(), куда я добавил комментарий.На самом деле, фигурных скобок в IL не существует.Они представляют собой синтаксический контракт между вами и компилятором вашего языка. Other.work() может прекратить сообщать о aC, как только он инициировал вызов aC.m().

Это совершенно нормально для вызова финализатора в вашем методе do work, поскольку после вызова ex.Hash среда 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.KeepAlive делает...ничего :) это пустой нестационарный / jittable метод, единственная цель которого - заставить GC думать, что объект будет использоваться после этого.

ПРЕДУПРЕЖДЕНИЕ:Ваш пример совершенно корректен, если бы метод DoWork был управляемым методом C ++...Ты ДЕЛАЙ вам придется вручную поддерживать управляемые экземпляры в рабочем состоянии, если вы не хотите, чтобы деструктор вызывался из другого потока.Т.е.вы передаете ссылку на управляемый объект, который собирается удалить большой двоичный объект неуправляемой памяти после завершения, и метод использует этот же большой двоичный объект.Если вы не поддерживаете экземпляр в рабочем состоянии, у вас возникнет состояние гонки между GC и потоком вашего метода.

И это закончится слезами.И управляемое повреждение кучи...

Полный Код

Ниже вы найдете полный код, скопированный / вставленный из файла Visual 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();
    }
}

Для тех, кому интересно, я могу отправить архивированный проект по электронной почте.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top