Вывод: C#/.NET предполагается сборщиком мусора. C# имеет деструктор, используемый для очистки ресурсов. Что происходит, когда объект A удаляется сборщиком мусора в той же строке, когда я пытаюсь клонировать один из его переменных-членов? Судя по всему, на мультипроцессорах иногда выигрывает сборщик мусора...
Проблема
Сегодня на тренировке по C# преподаватель показал нам код, который содержал ошибку только при запуске на мультипроцессорах.
Подводя итог, скажу, что иногда компилятор или JIT ошибаются, вызывая финализатор объекта класса C# перед возвратом из вызванного метода.
Полный код, приведенный в документации Visual C++ 2005, будет опубликован в качестве ответа, чтобы не задавать очень-очень большие вопросы, но основные из них приведены ниже:
Следующий класс имеет свойство Hash, которое возвращает клонированную копию внутреннего массива. При построении первый элемент массива имеет значение 2. В деструкторе его значение устанавливается равным нулю.
Дело в том, что если вы попытаетесь получить свойство Hash из примера, вы получите чистую копию массива, первый элемент которого по-прежнему равен 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 get. Так что в итоге мы имеем клон обнуленного массива байтов вместо правильного (с 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, несмотря на использование одного и того же кода в одних и тех же условиях (одновременное выполнение нескольких одинаковых исполняемых файлов, режим выпуска и т. д.)
!Example
, а не деструктора~Example
(который в C++/CLI создает реализацию IDisposable.) Это особенность C++/CLI, предназначенная для облегчения перехода для разработчиков C++, которые ожидают, что деструктор будет вызываться детерминировано, когда класс выходит за пределы области видимости или удаляется (в управляемом случае из C#, когда он выходит за рамки использования ' Заявление о ликвидации) - person Dan Bryant   schedule 18.07.2011Dispose()
иDispose(bool)
, которые очень сложно писать на C#. (не говоря уже о правильном написании на С#). RAII не является понятием C++. RAII — это шаблон, который правильно понимают немногие языки. C# (и тем более Java) потерпели неудачу в этом вопросе. - person paercebal   schedule 19.07.2011Dispose(bool)
является плохой рекомендацией для общего использования; Я обнаружил, что в большинстве случаев я могу просто пометить свой класс как запечатанный и напрямую реализовать Dispose(). - person Dan Bryant   schedule 19.07.2011sealed
не является опцией в коде, который я должен исправить (из-за того, что финализация / удаление приводит к сбою наших приложений .NET) ... И этот беспорядок занял у меня недели и снова займет недели, исправлен код удаления, который настолько уродлив, что я не могу смотреть на него без солнцезащитных очков, чтобы защитить глаза... Возможно, следующая итерация C# позволит нам использовать нотацию ~XXX/!XXX. В конце концов, если компилятор C++/CLI способен генерировать правильный кодFinalize
иDispose
, то и C# должен уметь это делать... - person paercebal   schedule 20.07.2011