Удаление MemoryCache в Finalizer вызывает AccessViolationException

ИЗМЕНИТЬ Дополнительную информацию см. в примечании к редактированию внизу вопроса.

Исходный вопрос

У меня есть класс CacheWrapper, который внутри создает и хранит экземпляр класса .NET MemoryCache.

MemoryCache подключается к событиям AppDomain, поэтому он никогда не будет удален сборщиком мусора, если он не будет удален явно. Вы можете проверить это с помощью следующего кода:

Func<bool, WeakReference> create = disposed => {
    var cache = new MemoryCache("my cache");
    if (disposed) { cache.Dispose(); }
    return new WeakReference(cache);
};

// with false, we loop forever. With true, we exit
var weakCache = create(false);
while (weakCache.IsAlive)
{
    "Still waiting...".Dump();
    Thread.Sleep(1000);
    GC.Collect();
    GC.WaitForPendingFinalizers();
}
"Cleaned up!".Dump();

Из-за такого поведения я считаю, что мой экземпляр MemoryCache следует рассматривать как неуправляемый ресурс. Другими словами, я должен убедиться, что он расположен в финализаторе CacheWrapper (CacheWrapper сам по себе является Disposable и следует стандартному шаблону Dispose(bool)).

Однако я обнаружил, что это вызывает проблемы, когда мой код запускается как часть приложения ASP.NET. Когда домен приложения выгружается, финализатор запускается в моем классе CacheWrapper. Это, в свою очередь, пытается удалить экземпляр MemoryCache. Здесь я сталкиваюсь с проблемами. Кажется, что Dispose пытается загрузить некоторую информацию о конфигурации из IIS, что не удается (предположительно, потому что я нахожусь в процессе выгрузки домена приложения, но я не уверен. Вот дамп стека, который у меня есть:

MANAGED_STACK: 
    SP               IP               Function
    000000298835E6D0 0000000000000001 System_Web!System.Web.Hosting.UnsafeIISMethods.MgdGetSiteNameFromId(IntPtr, UInt32, IntPtr ByRef, Int32 ByRef)+0x2
    000000298835E7B0 000007F7C56C7F2F System_Web!System.Web.Configuration.ProcessHostConfigUtils.GetSiteNameFromId(UInt32)+0x7f
    000000298835E810 000007F7C56DCB68 System_Web!System.Web.Configuration.ProcessHostMapPath.MapPathCaching(System.String, System.Web.VirtualPath)+0x2a8
    000000298835E8C0 000007F7C5B9FD52 System_Web!System.Web.Hosting.HostingEnvironment.MapPathActual(System.Web.VirtualPath, Boolean)+0x142
    000000298835E940 000007F7C5B9FABB System_Web!System.Web.CachedPathData.GetPhysicalPath(System.Web.VirtualPath)+0x2b
    000000298835E9A0 000007F7C5B99E9E System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x2ce
    000000298835EB00 000007F7C5B99E19 System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x249
    000000298835EC60 000007F7C5BB008D System_Web!System.Web.Configuration.HttpConfigurationSystem.GetApplicationSection(System.String)+0x1d
    000000298835EC90 000007F7C5BAFDD6 System_Configuration!System.Configuration.ConfigurationManager.GetSection(System.String)+0x56
    000000298835ECC0 000007F7C63A11AE System_Runtime_Caching!Unknown+0x3e
    000000298835ED20 000007F7C63A1115 System_Runtime_Caching!Unknown+0x75
    000000298835ED60 000007F7C639C3C5 System_Runtime_Caching!Unknown+0xe5
    000000298835EDD0 000007F7C7628D86 System_Runtime_Caching!Unknown+0x86
    // my code here

Есть ли известное решение этого? Правильно ли я думаю, что мне нужно удалить MemoryCache в финализаторе?

ИЗМЕНИТЬ

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

Вообще говоря, финализаторы не могут получить доступ к управляемым объектам. Однако поддержка логики выключения необходима для достаточно сложного программного обеспечения. Пространство имен Windows.Forms обрабатывает это с помощью Application.Exit, который инициирует упорядоченное завершение работы. При разработке библиотечных компонентов полезно иметь способ поддержки логики завершения работы, интегрированный с существующим логически подобным IDisposable (это позволяет избежать необходимости определять интерфейс IShutdownable без какой-либо встроенной языковой поддержки). Обычно это делается путем поддержки упорядоченного завершения работы, когда вызывается IDisposable.Dispose, и аварийного завершения работы, когда это не так. Было бы еще лучше, если бы финализатор можно было использовать для упорядоченного завершения работы, когда это возможно.

Microsoft тоже столкнулась с этой проблемой. Класс StreamWriter владеет объектом Stream; StreamWriter.Close очистит свои буферы, а затем вызовет Stream.Close. Однако, если StreamWriter не был закрыт, его финализатор не может очистить свои буферы. Microsoft «решила» эту проблему, не предоставляя StreamWriter финализатор, надеясь, что программисты заметят недостающие данные и выведут свою ошибку. Это прекрасный пример необходимости логики выключения.

Все сказанное, я думаю, должно быть возможно реализовать "управляемую финализацию" с помощью WeakReference. По сути, пусть ваш класс зарегистрирует WeakReference для себя и действие finalize с некоторой очередью при создании объекта. Затем очередь отслеживается фоновым потоком или таймером, который вызывает соответствующее действие при сборе парного WeakReference. Конечно, вы должны быть осторожны, чтобы ваше действие finalize случайно не удерживало сам класс, тем самым полностью предотвращая сбор!


person ChaseMedallion    schedule 08.10.2014    source источник
comment
Я почти уверен, что вы никогда не должны удалять управляемый объект в финализаторе, вместо этого это следует делать в обычном IDisposable.Dispose. Финализатор должен очищать только неуправляемые объекты.   -  person juharr    schedule 08.10.2014
comment
@juharr: в этом случае, однако, объект неуправляемый в том смысле, что он НИКОГДА не будет собирать мусор, если он не будет удален явно.   -  person ChaseMedallion    schedule 08.10.2014
comment
@juharr: управляемый ресурс — это объект, который содержит внешние ресурсы, но освобождает их, если от него отказываются. Вещи, которые удерживают внешние ресурсы, но не освобождают их, если их забрасывают, должны рассматриваться как неуправляемые ресурсы, хотя во многих случаях их нельзя очистить с помощью финализаторов, и поэтому единственным средством является обеспечение что они никогда не будут брошены в первую очередь.   -  person supercat    schedule 09.10.2014


Ответы (2)


Вы не можете Dispose управляемых объектов в финализаторе, так как они могут быть уже завершены (или, как вы видели здесь, части среды могут больше не находиться в ожидаемом состоянии). Это означает, что если вы содержать класс, который должен быть удален явно, ваш класс также должен быть удален явно. Невозможно «обмануть» и сделать Disposal автоматическим. К сожалению, сборка мусора в таких случаях является дырявой абстракцией.

person Dan Bryant    schedule 08.10.2014
comment
Интересный. Есть ли какая-либо явная документация для этого правила? В тех вещах, которые я читал, всегда используется термин неуправляемые ресурсы, который, как я понял, означает все, что не может быть очищено сборщиком мусора. Означает ли это также, что вы можете работать только с дескрипторами ОС в финализаторах? Например, если бы у меня был строковый путь к файлу, который я хотел удалить, допустимо ли, чтобы мой финализатор ссылался на эту строку, чтобы удалить этот файл? - person ChaseMedallion; 08.10.2014
comment
Вы действительно не должны запускать какой-либо код в своем финализаторе, который явно не закрывает неуправляемый ресурс с помощью дескриптора, такого как IntPtr. Выполнение любого типа операций ввода-вывода в потоке финализатора — очень плохая идея, и это может привести именно к тем проблемам, которые вы видите. Вы понятия не имеете, каким будет контекст безопасности при запуске финализатора. - person Mike Strobel; 08.10.2014
comment
@MikeStrobel спасибо за дополнительную информацию. Можете ли вы указать на какую-либо официальную документацию для этого? - person ChaseMedallion; 09.10.2014
comment
Насколько я могу судить, MS никогда не определяет неуправляемый ресурс. Он предлагает примеры, но не определение. Я бы определил это так, как вам кажется: внешний ресурс, который не будет автоматически очищаться, если объект будет заброшен. - person supercat; 09.10.2014

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

Единственный случай, когда финализатору действительно имеет смысл пытаться очистить ресурс, принадлежащий другому объекту, — это когда другой объект разработан для взаимодействия с финализатором. Я не могу вспомнить ни одного места, где классы Framework были спроектированы с правильными ловушками, но приведу пример того, как Microsoft могла спроектировать их для этого.

Объект File может предложить событие с потокобезопасными методами подписки и отмены подписки, которые сработают (сначала уведомляя последнего подписчика), когда объект File получит либо вызов Dispose, либо запрос finalize. Событие будет срабатывать между моментом вызова Finalize и моментом фактического закрытия инкапсулированного файла, и может использоваться классом внешней буферизации в качестве сигнала о том, что ему необходимо предоставить File любую информацию, которую он получил, но еще не передал. вместе.

Обратите внимание, что для обеспечения правильной и безопасной работы такой вещи необходимо, чтобы часть объекта File, в которой есть финализатор, не была доступна публике, и чтобы он использовал длинную слабую ссылку, чтобы гарантировать, что если он запустится во время общедоступный объект все еще жив, он перерегистрирует себя для финализации. Обратите внимание, что если единственная ссылка на объект WeakReference хранится в финализируемом объекте, его свойство Target может стать недействительным, если финализируемый объект станет доступным для финализации, даже если фактическая цель ссылки все еще жива. Дефектный дизайн, ИМХО, и тот, который нужно обработать несколько осторожно.

Можно создавать объекты с финализаторами, которые могут взаимодействовать (самый простой способ сделать это, как правило, иметь только один объект в группе с финализатором), но если вещи не предназначены для взаимодействия с финализаторами, лучше всего Что можно сделать, так это заставить финализатор подать сигнал тревоги, указывающий: «Этот объект не должен был быть Disposed, но не был; поскольку это не так, ресурсы будут утекать, и с этим ничего нельзя сделать, кроме как исправить код правильно распорядиться объектом в будущем».

person supercat    schedule 09.10.2014