Защо не трябва да използватефинализатори
Не много отдавна работихме върху диагностика, свързана с проверка на финализатора, и един колега и аз имахме спор относно детайлите на събирача на отпадъци и финализирането на обекти. И въпреки че ние с него се развиваме в C # повече от 5 години, не стигнахме до общо мнение и реших да проуча този въпрос по-подробно.
Обикновено .NET разработчиците първо се запознават с финализаторите, когато трябва да освободят неуправляван ресурс. Възниква въпросът какво трябва да се използва: внедряване на IDisposable във вашия клас или добавяне на финализатор? След това отиват, например, в StackOverflow и четат отговори на въпроси като този модел Finalize / Dispose в C #, който говори за класическия шаблон за внедряване IDisposable в комбинация с дефиницията на финализатора. Същият модел може да бъде намерен в MSDN в описанието на интерфейса IDisposable. Някои смятат, че е доста трудно да разберат и предлагат свои собствени опции, като прилагане на почистването на управлявани и неуправлявани ресурси в отделни методи или създаване на обвиващ клас специално за освобождаване на неуправляван ресурс. Те могат да бъдат намерени на същата страница в StackOverflow.
Повечето от тези методи включват внедряване на финализатор. Нека видим какви предимства и потенциални проблеми може да донесе това.
Плюсове и минуси на използването на финализатори
- Финализаторът ви позволява да почистите обект, преди да бъде събран на боклука. Ако разработчикът е забравил да извика метода Dispose() на обекта, тогава финализаторът може да освободи неуправляваните ресурси и по този начин да избегне изтичането им.
Може би всичко. Това е единственият плюс и дори тогава спорен, както е обсъдено по-долу.
- Финализирането е недетерминирано. Не знаете кога ще бъде извикан финализаторът. Преди CLRзапочне финализиране на обекти, събирачът на боклук трябва да ги постави в опашка от обекти, готови за финализиране, когато започне следващото събиране на боклук. Този момент не е дефиниран.
- Поради факта, че обектът с финализатора не се премахва незабавно от събирача на отпадъци, той и цялата графа от обекти, свързани с него, оцеляват след събирането на отпадъци и попадат в следващото поколение. Те ще бъдат изтрити сега, когато събирачът на боклук реши да събере обекти от това поколение, което може да не се случи много скоро.
- Тъй като финализаторите работят в отделна нишка паралелно с други нишки в приложението, може да се случи нови обекти, които изискват финализиране, да бъдат създадени по-бързо, отколкото ще бъдат обработени финализаторите на стари обекти. Това ще доведе до увеличено потребление на памет, по-бавна производителност и евентуално срив на приложението с OutOfMemoryException. Освен това на машина за разработчици може никога да не срещнете тази ситуация, например, защото имате по-малко процесори и обектите се създават по-бавно или приложението не работи толкова дълго, колкото в бойни условия, и паметта няма време да изтече. Може да отнеме много време, за да разберете, че причината е във финализаторите. Този минус може би припокрива предимствата на един плюс.
- Ако възникне изключение по време на изпълнение на финализатора, приложението ще прекрати внезапно. Следователно, когато внедрявате финализатор, трябва да бъдете особено внимателни: не извиквайте методите на други обекти, за които финализаторът вече може да бъде извикан; вземете предвид, че финализаторът се извиква в отделна нишка; проверете за null всички други обекти, които потенциално биха могли да бъдат null. Последното правило е свързано с факта, че може да бъде извикан финализаторобект във всяко от неговите състояния, дори не е напълно инициализиран. Например, ако винаги присвоявате нов обект в поле на клас в конструктор и след това очаквате, че той винаги трябва да е различен от null във финализатора и имате достъп до него, можете да получите NullReferenceException, ако възникне изключение, докато създавате обект в конструктора на базовия клас и той не е стигнал до изпълнението на вашия конструктор.
- Финализаторът може изобщо да не се изпълни. Ако приложението прекрати необичайно, например ако възникне изключение в нечий друг финализатор поради причините, описани в предходния параграф, всички други финализатори няма да бъдат изпълнени. Ако освободите неуправлявани обекти на операционната система във финализатора, тогава няма да се случи нищо лошо в смисъл, че когато приложението приключи, самата система ще върне ресурсите си. Но ако изхвърлите гарантираните байтове във файл, тогава ще загубите данните си. Така че вероятно е най-добре да не прилагате финализатор, но винаги да позволявате загуба на данни, ако сте забравили да извикате Dispose(), тъй като това ще направи проблема по-лесен за откриване.
- Имайте предвид, че финализаторът се извиква само веднъж и ако възкресявате обект във финализатора, като присвоите препратка към него към друг жив обект, тогава може да се наложи да го регистрирате за финализиране отново с помощта на метода GC. ReRegisterForFinalize() .
- Можете да се сблъскате с проблеми с многонишкови приложения, като например състезателни условия, дори ако приложението ви е еднонишково. Случаят е доста екзотичен, но теоретично възможен. Да предположим, че вашият обект има финализатор и друг обект съдържа препратка към него, който също има финализатор. Ако и двата обекта станат достъпни за събирача на отпадъци и техните финализатори стартиратбягайте и другият обект е възкресен, след което той и вашият обект отново оживяват. Сега е възможно методът на вашия обект да бъде извикан от основната нишка и от финализатора едновременно, тъй като той все още е в опашката от обекти, готови за финализиране. Кодът, който възпроизвежда този пример, е показан по-долу. Можете да видите как първо се изпълнява финализаторът на обекта Root, след това финализаторът на обекта Nested и след това методът DoSomeWork() се извиква от две нишки едновременно.
Ето какво ще се покаже на моята машина:
Ако вашите финализатори се извикват в различен ред, опитайте да размените създаването на вложени и root.
Финализаторите в .NET са най-лесното място да се простреляте в крака. Преди да побързате да добавите финализатори за всички класове, които имплементират IDisposable, струва си да помислите дали наистина са необходими. Трябва да се отбележи, че самите разработчици на CLR предупреждават срещу използването им на страницата Dispose Pattern: "Избягвайте да правите типове финализируеми. Внимателно обмислете всеки случай, в който смятате, че е необходим финализатор. Има реална цена, свързана с екземпляри с финализатори, както от гледна точка на производителността, така и от гледна точка на сложността на кода."
Но ако все пак решите да използвате финализатори, тогава PVS-Studio може да ви помогне да намерите потенциални грешки. Имаме V3100 диагностика, която ще покаже всички места във финализатора, където може да възникне NullReferenceException.
Намерете грешки във вашия C, C++, C# и Java код
Предлагаме да опитате да проверите кода на вашия проект с помощта на анализатора на код PVS-Studio. Една грешка, намерена в него, ще ви каже повече за предимствата на методологията за анализ на статичен код, отколкото дузина статии.