2014-10-08 35 views
8

EDIT Zobacz komentarz edytujący na dole pytania, aby uzyskać dodatkowe informacje.Utylizacja MemoryCache w Finalizerze zgłasza wyjątek AccessViolationException

Original pytanie

Mam klasy CacheWrapper która tworzy i utrzymuje na wystąpienie klasy .NET MemoryCache wewnętrznie.

MemoryCache dołącza się do zdarzeń AppDomain, więc nigdy nie będzie zbierane śmieci, chyba że zostanie wyraźnie usunięty. Można to sprawdzić za pomocą następującego kodu:

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(); 

Ze względu na zachowanie, wierzę, że moje wystąpienie MemoryCache powinny być traktowane jako zasób niekontrolowana. Innymi słowy, powinienem upewnić się, że jest on umieszczony w finalizatorze CacheWrapper (sama CacheWrapper jest jednorazowa, zgodnie ze standardowym wzorem Dispose (bool)).

Jednak stwierdzam, że powoduje to problemy, gdy mój kod działa jako część aplikacji ASP.NET. Po wyładowaniu domeny aplikacji finalizator działa na mojej klasie CacheWrapper. To z kolei próbuje zlikwidować instancję MemoryCache. To tutaj napotykam na problemy. Wydaje się, że Dispose próbuje załadować informacje konfiguracyjne z IIS, która nie (prawdopodobnie dlatego, że jestem w środku rozładunku domenę aplikacji, ale nie jestem pewien Oto zrzut stosu mam.

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 

Czy istnieje znany rozwiązaniem tego problemu? Czy mam rację sądząc, że nie trzeba wyrzucać MemoryCache w finalizatora?

EDIT

This article sprawdza odpowiedź Dan Bryanta i omawia wiele interesujących szczegółów. W szczególności W tym przypadku obejmuje on przypadek StreamWriter, który ma podobny scenariusz do mojego, ponieważ chce go opróżnić podczas usuwania. Oto, co mówi artykuł:

Ogólnie rzecz biorąc, finalizatorzy mogą nie mieć dostępu do zarządzanych obiektów. Jednak obsługa logiki zamykania jest niezbędna w przypadku rozsądnie złożonego oprogramowania . Przestrzeń nazw Windows.Forms obsługuje to z Application.Exit, który inicjuje uporządkowane zamykanie. Kiedy projektujemy komponenty biblioteki, pomocne jest posiadanie sposobu, w jaki wspiera logikę zamykania zintegrowaną z istniejącym logicznie podobnym IDisposable (unikając konieczności definiowania interfejsu bez wbudowanej obsługi języka). Ta jest zwykle wykonywana przez obsługę regularnego wyłączania, gdy wywoływana jest opcja IDisposable.Dispose, a nieudane wyłączenie, gdy nie jest to . Byłoby jeszcze lepiej, gdyby finalizator mógł być użyty do rutynowego wyłączenia, gdy tylko było to możliwe.

Microsoft również wystąpił przeciwko temu problemowi. Klasa StreamWriter jest właścicielem obiektu Stream; StreamWriter.Close opróżni swoje bufory i , a następnie wywoła Stream.Close. Jeśli jednak StreamWriter nie został zamknięty, jego finalizator nie może opróżnić swoich buforów.Microsoft "rozwiązał" ten problem, nie dając StreamWriterowi finalizatora, mając nadzieję, że programiści uzyskają brakujące dane i wyprowadzą ich błąd. Jest to idealny przykład potrzeby logiki wyłączania.

Uważam, że powinno być możliwe wdrożenie "zarządzanej finalizacji" za pomocą WeakReference. Zasadniczo, niech twoja klasa zarejestruje WeakReference dla siebie i sfinalizuje akcję z jakąś kolejką, kiedy obiekt zostanie utworzony. Kolejka jest następnie monitorowana przez wątek tła lub licznik czasu, który wywołuje odpowiednią akcję, gdy jest sparowany, WeakReference zostaje zebrany. Oczywiście, musisz uważać, aby Twoja sfinalizowana akcja nie zatrzymała się na samej klasie, co całkowicie uniemożliwiło jej zbieranie!

+0

Dlaczego spadamy? – ChaseMedallion

+2

Jestem prawie pewny, że nigdy nie należy pozbywać się zarządzanego obiektu w finalizatorze, zamiast tego powinno się to odbywać w normalnym 'IDisposable.Dispose'. Finalizator powinien tylko czyścić niezarządzane obiekty. – juharr

+2

@juharr: W tym przypadku obiekt jest "niezarządzany" w tym sensie, że NIGDY nie będzie zbierany śmieci, chyba że zostanie wyraźnie usunięty. – ChaseMedallion

Odpowiedz

7

Nie można wyrzucać zarządzanych obiektów w finalizatorze, ponieważ mogły już zostać sfinalizowane (lub, jak widzieliśmy tutaj, części środowiska mogą już nie być w oczekiwanym stanie). oznacza, że ​​jeśli zawierasz klasę, która musi być wyraźnie zignorowana, twoja klasa musi być również wyraźnie usunięta. Nie ma sposobu, aby "oszukać" i uczynić Utylizację automatyczną. Niestety, w takich przypadkach, odśmiecanie jest nieszczelną abstrakcją.

+1

Interesujące. Czy istnieje wyraźna dokumentacja dla tej reguły? W rzeczach, które czytałem, zawsze używam terminu "zasoby niezarządzane", które miałem na myśli, a które nie mogły zostać usunięte przez GC. Czy to też oznacza, że ​​zasadniczo można pracować tylko z uchwytami systemu operacyjnego w finalizatorach? Na przykład, gdybym miał ścieżkę łańcucha dla pliku, który chciałem usunąć, czy byłby prawidłowy dla mojego finalizatora, aby odwoływał się do tego ciągu w celu usunięcia tego pliku? – ChaseMedallion

+0

Naprawdę nie powinieneś uruchamiać żadnego kodu w swoim finalizatorze, który nie zamyka jawnie zasobu niezarządzanego za pomocą uchwytu takiego jak 'IntPtr'. Wykonywanie wszelkiego rodzaju operacji wejścia/wyjścia w wątku finalizatora jest bardzo złym pomysłem i może prowadzić do dokładnie tego rodzaju problemów, które widzisz. Nie masz pojęcia, jaki będzie kontekst bezpieczeństwa, gdy uruchomi się twój finalizator. –

+0

@MikeStrobel dzięki za dodatkowy szczegół. Czy możesz wskazać na jakąkolwiek oficjalną dokumentację? – ChaseMedallion

1

Sugerowałbym, że obiekty z finalizatorami nie powinny być ogólnie eksponowane na świat zewnętrzny i powinny zawierać silne odniesienia do rzeczy, które są rzeczywiście potrzebne do sfinalizowania i nie są wystawione na nic w świecie zewnętrznym, który ich nie oczekuje. do wykorzystania w tym celu. Typy publiczne nie powinny mieć samych finalizatorów, ale powinny raczej zawierać logikę czyszczenia wewnątrz prywatnych instancji klas finalizowalnych, których zadaniem jest enkapsulacja takiej logiki.

Jedynym momentem, w którym finalizator rzeczywiście próbuje wyczyścić zasób należący do innego obiektu, jest sytuacja, w której drugi obiekt jest przeznaczony do połączenia z finalizatorem. Nie mogę wymyślić żadnych miejsc, w których klasy ramowe zostały zaprojektowane w odpowiednich hakach, ale będą przykładem, w jaki sposób Microsoft mógł zaprojektować je w taki sposób.

File obiekt może zaoferować imprezę z wątku bezpieczny zapisywanie i wypisywanie metody, które przeciwpożarowa (powiadamiając ostatniego abonenta pierwszy), gdy obiekt File odbiera albo Dispose wezwanie lub wniosek finalize. Zdarzenie zostanie wywołane między czasem wywołania Finalize a czasem zamknięcia pliku enkapsulowanego i może być użyte przez zewnętrzną klasę buforowania jako sygnał, który musi podać File jakiejkolwiek informacji, którą otrzymał, ale jeszcze nie zdał wzdłuż.

Pamiętaj, że aby coś takiego działało poprawnie i bezpiecznie, konieczne jest, aby część obiektu File, która ma finalizator, nie była publicznie dostępna, i aby używał długiego, słabego odniesienia, aby zapewnić, że działa, gdy obiekt publiczny jest nadal żywy, zostanie ponownie zarejestrowany do finalizacji. Zauważ, że jeśli jedyne odniesienie do obiektu WeakReference jest przechowywane w obiekcie finalizowanym, jego właściwość Target może zostać unieważniona, jeśli obiekt finalizowalny kwalifikuje się do finalizacji , nawet jeśli rzeczywisty cel odwołania nadal istnieje. Wadliwy projekt, IMHO, i taki, który należy nieco rozejrzeć.

Możliwe jest projektowanie obiektów za pomocą finalizatorów, które mogą współpracować (najłatwiej to zrobić, ogólnie rzecz biorąc, ma tylko jeden obiekt w grupie sportowej finalizatora), ale jeśli rzeczy nie są zaprojektowane do współpracy z finalizatorami, najlepsze, co można zrobić, to generalnie dźwięk finalizatora alarmowy wskazujący "Ten obiekt nie powinien być, ale nie był, ponieważ zasoby nie będą przeciekać, i nie ma nic do zrobienia to oprócz naprawienia kodu, aby w przyszłości poprawnie usunąć obiekt ".