Финализатор запущен, пока его объект еще использовался
Резюме: C# /.NET должен быть сборщиком мусора. В C# есть деструктор, используемый для очистки ресурсов. Что произойдет, когда объект A будет собран мусором в той же строке, что и я, пытаясь клонировать один из его переменных-членов? Видимо, на мультипроцессорах иногда сборщик мусора побеждает...
Эта проблема
Сегодня на тренинге по C# учитель показал нам некоторый код, который содержал ошибку только при работе на мультипроцессорах.
Подводя итог, скажу, что иногда компилятор или JIT облажаются, вызывая финализатор объекта класса C# перед возвратом из вызываемого метода.
Полный код, приведенный в документации по Visual C++ 2005, будет опубликован как "ответ", чтобы не задавать очень очень большие вопросы, но основные из них приведены ниже:
Следующий класс имеет свойство "Hash", которое будет возвращать клонированную копию внутреннего массива. При построении первый элемент массива имеет значение 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" получить метод. Итак, в итоге мы получим клон массива байтов с нулевым редактированием вместо правильного (с первым элементом в 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 +1...
сожалею
:-)
Редактировать 2
Мне не удалось воспроизвести проблему в Linux/Ubuntu/Mono, несмотря на использование одного и того же кода в одинаковых условиях (одновременное выполнение нескольких исполняемых файлов, режим выпуска и т. Д.)
8 ответов
Это просто ошибка в вашем коде: финализаторы не должны получать доступ к управляемым объектам.
Единственная причина для реализации финализатора - освободить неуправляемые ресурсы. И в этом случае вам следует тщательно реализовать стандартный шаблон IDisposable.
С помощью этого шаблона вы реализуете защищенный метод "защищенное удаление (удаление bool)". Когда этот метод вызывается из финализатора, он очищает неуправляемые ресурсы, но не пытается очистить управляемые ресурсы.
В вашем примере у вас нет неуправляемых ресурсов, поэтому не следует реализовывать финализатор.
То, что вы видите, совершенно естественно.
Вы не сохраняете ссылку на объект, которому принадлежит байтовый массив, так что этот объект (не байтовый массив) фактически свободен для сборщика мусора.
Сборщик мусора действительно может быть таким агрессивным.
Поэтому, если вы вызываете метод для вашего объекта, который возвращает ссылку на внутреннюю структуру данных, и финализатор для вашего объекта портит эту структуру данных, вам также необходимо сохранить живую ссылку на объект.
Сборщик мусора видит, что переменная ex больше не используется в этом методе, поэтому он может и, как вы заметили, собирает мусор при правильных обстоятельствах (т. Е. Времени и необходимости).
Правильный способ сделать это - вызвать GC.KeepAlive для ex, поэтому добавьте эту строку кода в конец вашего метода, и все должно быть хорошо:
GC.KeepAlive(ex);
Я узнал об этом агрессивном поведении, прочитав книгу Джеффри Рихтера " Прикладное программирование на .NET Framework ".
Это похоже на состояние гонки между вашим рабочим потоком и потоком (ами) GC; Чтобы избежать этого, я думаю, что есть два варианта:
(1) измените ваш оператор if, чтобы использовать ex.Hash[0] вместо res, чтобы ex не мог быть преждевременным GC, или
(2) блокировка ex на время вызова Hash
это довольно изящный пример - учитель считал, что в компиляторе JIT может быть ошибка, которая проявляется только в многоядерных системах, или что этот вид кодирования может иметь тонкие условия гонки со сборкой мусора?
Я думаю, что вы видите разумное поведение из-за того, что все работает в нескольких потоках. Это является причиной метода GC.KeepAlive(), который должен использоваться в этом случае, чтобы сообщить GC, что объект все еще используется и что он не является кандидатом на очистку.
Глядя на функцию DoWork в ответе "полный код", проблема в том, что сразу после этой строки кода:
byte[] res = ex.Hash;
функция больше не делает никаких ссылок на ex- объект, поэтому она становится пригодной для сборки мусора в этот момент. Добавление вызова к GC.KeepAlive предотвратит это.
Интересный комментарий из блога Криса Брамма
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() в какой-то другой метод и сообщить о C еще дольше. Даже если вы добавите "aC = null;" после его использования, JIT может считать это назначение мертвым кодом и устранять его. Независимо от того, когда JIT перестает сообщать о ссылке, GC может не успеть собрать ее в течение некоторого времени.
Еще интереснее беспокоиться о самом раннем моменте, когда можно собрать aC. Если вы похожи на большинство людей, вы догадаетесь, что самый быстрый aC получит право на сбор средств - в заключительной скобке предложения if.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++... Вы ДОЛЖНЫ вручную поддерживать управляемые экземпляры вручную, если не хотите, чтобы деструктор вызывался из другого потока. IE. вы передаете ссылку на управляемый объект, который собирается завершить обработку BLOB-объектов неуправляемой памяти после завершения, и метод использует этот же BLOB-объект. Если вы не удержите экземпляр живым, у вас будет условие состязания между 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();
}
}
Для тех, кто заинтересован, я могу отправить ZIP-проект по электронной почте.