Управление на паметта и ресурсите


Ръчно управление на ресурсите с IDisposable



страница4/7
Дата25.02.2017
Размер0.85 Mb.
#15739
1   2   3   4   5   6   7

Ръчно управление на ресурсите с IDisposable





Люк: Бъдещето ли? Ще умрат ли?




Йода: Трудно е да се види. Бъдещето винаги е в движение.

Голям недостатък на системата за почистване на паметта е, че няма абсолютно никаква гаранция кога ще бъде почистена паметта, нито кога ще бъдат извикани финализаторите на недостижимите обекти.

Обикновено това не бива да ви притеснява, но ако вашия обект е обвивка около неуправляван ресурс на операционната система (например файл, сокет, връзка към база данни и т.н.) в повечето случаи ще искате да освободите ресурса, за да може да го използват и други приложения или друга нишка във вашето приложение.

Не само това, но и ръчното управление на ресурсите може да бъде по-ефективно. Например, ако пишете код използващ графичен ресурс (например четка или шрифт) и знаете, че от един определен момент ресурсът вече не ви е необходим, не само е добре да го освободите, тъй като той консумира неявно памет, но и е почти задължително да го освободите, тъй като почти всички ресурси на операционната система са ограничени9.

Както винаги, в .NET Framework съществува решение на проблема.


Интерфейсът IDisposable


Интерфейсът IDisposable се препоръчва от Microsoft в тези случаи, в които искате да гарантирате моментално освобождаване на ресурсите (вече знаете, че използването на Finalize() не го гарантира).

Използването на IDisposable се състои в имплементирането на интер­фейса от класа, който обвива някакъв неуправляван ресурс и освобожда­ването на ресурса при извикване на метода Dispose(). Ето как изглежда този интерфейс:



public interface IDisposable

{

void Disposе();



}

Използването на IDisposable обекти е тривиално, но ще ви покажем правилната употреба, когато очаквате възникване на изключение, тъй като сме виждали няколко погрешни практики, които не искаме да научите:

// Придобиваме ресурса

Resource resource = new Resource();

try

{

// Използваме ресурса



}

finally


{

// Унищожаваме (освобождаваме) ресурса.

// Преобразуването на обекта към IDisposable е добра практика

// тъй като обектът може да имплементира интерфейса

// експлицитно както ще видите по-нататък

((IDisposable)resource).Dispose();

}

Операторът using


Тази употреба се среща толкова често, че Microsoft са добавили в езика C# оператора using, който прави същото без изричното споменаване на оператори и клаузи за работа с изключения:

Resource resource = new Resource();

using (resource)

{

// Използваме ресурса



}

Операторът се превръща от C# компилатора в следния код:

Resource resource = new Resource();

try


{

}

finally



{

if (resource != null)

{

((IDisposable)resource).Dispose();



}

}


Естествено, компилаторът е достатъчно интелигентен, за да премахне проверката за null преди да извика Dispose() в простите случаи като горния.

Ние ви препоръчваме да създадете ресурса в израза, поместен в скобите на оператора using, като по този начин ще намалите видимостта на променливата resource (което и ще искате да направите в повечето от случаите):



// Придобиваме ресурса

using (Resource resource = AcquireResource())

{

// Използваме ресурса



}
// Ресурсът e освободен

Съществува обаче проблемът да забравите да извикате Dispose() на обекта (или клиент на вашата библиотека с ресурси да забрави да го направи) и тогава, в най-добрия случай ще изтече памет. На помощ идват финализаторите, които могат да ви гарантират това извикване на метода Dispose() (ако вече не е извикан).

IDisposable и Finalize





Винаги са двама, не повече, не по-малко: майстор и ученик. Йода

На практика съществуват няколко въпроса, на които трябва да си отговорим преди да пристъпим към имплементация на IDisposable и написването на финализатор:

Нуждае ли се класът ни от експлицитно ръчно управление?


Повечето начинаещи .NET програмисти (да не се бърка с начинаещ програмист), особено тези които имат предишен опит с езици, при които паметта се управлява ръчно (например C и C++) тайно се надяват отговорът да е “да”. Уви, както при много други въпроси, свързани с програмирането, отговора е “зависи”, но в повечето случаи е “не”.

Правилният въпрос е “Обвива ли класът ни неуправляван ресурс?”.

По-напредналите .NET програмисти ще ви кажат, че ако класът ви използва неуправляван ресурс, задължително трябва да има финализатор и да имплементира IDisposable. Проблемът в подобно изказване е думичката “използва”. Например, ако класът ви използва неуправляван ресурс, който вече е обвит в управляван клас, не само не е необходимо да създадете повторен клас за обвиването на ресурса, но това може да доведе и до проблеми, например:

class BadPractice

{

~BadPractice()



{

mEventLog.Close(); //или ((IDisposable)mEventLog).Dispose()

}
private EventLog mEventLog; // инициализиран някъде другаде

}


По време на извикване на финализатора на BadPractice, обектът, сочен от полето mEventLog вече може да е “събран” от garbage collector и дори може да се е изпълнил финализаторът му, при което ще получите изхвър­ляне на изключението ObjectDisposedException.

Ако заменим “използва” с “обвива”, тогава ние също ви препоръчваме имплементирането и на финализатор и на интерфейса IDisposable.


Може ли да разчитаме на коректна употреба?


Разумно ли е да разчитаме, че класът ни ще бъде използван коректно и ако не, може ли да го защитим от неправилна употреба? Краткия отговор е “не и да”. Не само, че не можете да разчитате на коректна употреба на вашите класове, а е почти гарантирано, че ще бъдат използвани неправилно, при това находчиво неправилно, от по-неопитни програ­мисти. Добрата новина е, че можете да защитите класовете си срещу неправилна употреба, а лошата е, че в тази глава ще ви покажем как да го направите, но само в контекста на имплементацията на интерфейса IDisposable и финализатора на класа.

Съществуват няколко начина за неправилна употреба на вашия клас:



  • Потребителят на класа да забрави да извика Dispose()

  • Потребителят на класа да извика Dispose() повече от един път

  • Потребителят на класа да извика и Dispose() и Finalize() (евентуално повече от един път)

Ако се съмнявате във втория начин, просто си представете, че програмист е ползвал изрично извикване на Dispose(), по-късно се е научил да ползва оператора using и е забравил да премахне изричното извикване. Ако се съмнявате, че някой може да постигне третата неправилна употреба, се върнете на текста за финализация и по-специално на техниката “съживяване на обекти”.

Трябва ли да напишем кода си безопасен за употреба от многонишкови приложения?


Този въпрос няма правилен отговор. Не може да контролираме по никакъв начин броя на нишките, използващи една и съща инстанция на нашия клас. Отговорът се диктува от дизайна, който сте избрали. Ако обаче сте избрали да поддържате извиквания към обекта ви от много нишки, тогава считайте, че трябва да поддържате това както от финализатора, така и от метода Dispose(). Освен това имайте предвид факта, че кодът в нишките може да бъде прекъснат в най-неподходящ за вас момент, а именно по време на освобождаване на ресурсите.

Примерна имплементация на базов клас, обвиващ неуправляван ресурс


Имплементирането на интерфейса IDisposable изглежда измамливо лесно, тъй като този интерфейс съдържа един единствен метод, който не приема параметри и не връща резултат. Същото се отнася и до т. нар. финализатор.

Написването на код, който да се използва като шаблон е добра практика, но написването на код, който е готов да бъда използван (и преизползван) без модификация е много по-удобно и води до по-малко грешки. Ето защо ви предлагаме имплементация на базов клас, който има следните предим­ства:



  • Класът е завършен и единственото, което трябва да направите е да го наследите (това разбира се е и основният му недостатък)

  • Защитен е от всички възможни грешки от неправилна употреба

  • Поддържа многонишкови приложения

  • Защитава обекта от неочаквано прекъсване на нишката, в която се изпълнява унищожаването на ресурса10

// Базов клас за обекти, обвиващи ресурс

public abstract class ResourceWrapperBase : IDisposable

{

// Член-променливи и константи



private const int FALSE = 0;

private const int TRUE = 1;

private int mDisposed = FALSE;
~ResourceWrapperBase()

{

DisposeImpl(false);



}
// Имплементация на интерфейса IDisposable

public void Dispose()

{

DisposeImpl(true);



}
// Същинската имплементация е скрита в т. нар. шаблонен

// метод11

private void DisposeImpl(bool aDisposing)

{

// Проверяваме дали обектът вече не е бил освободен и



// веднага вдигаме флага, за да предотвратим паралелното

// изпълнение на същия код от друга нишка

if (Interlocked.CompareExchange(ref mDisposed,

TRUE, FALSE) == TRUE)

{

return;


}
// Отваряме try...finally блок за да се предпазим

// от възможността възникването на асинхронно изключение

// да прекъсне кода за освобождаването на ресурсите

// в неподходящ момент

try

{

if (aDisposing)



{

// Експлицитно освобождаване: делегираме към наследника

DisposeManagedResources();

}
// Отваряме try...catch блок за да предотвратим

// възможността нашият код да предизвика изключение

// по време на финализация

try

{

// Делегираме към наследника



DisposeUnmanagedResources();

}

catch



{

if (aDisposing)

{

// Изхвърляме повторно изключението, ако



// обектът не се финализира в момента

throw;


}

}

}



finally

{

if (aDisposing)



{

// Съобщаваме на системата за почистване на боклука, че

// обектът вече не се нуждае от финализация

GC.SuppressFinalize(this);

}

}

}


// Функция за проверка дали обектът вече е освободен

// предназначена да се ползва от наследниците на класа

protected bool IsDisposed()

{

return Interlocked.CompareExchange(ref mDisposed,



FALSE, FALSE) == FALSE;

}
// Помощна функция, предназначена за наследниците на класа,

// която трябва да бъде извикана във всички не-private методи protected void EnsureNotDisposed()

{

if (IsDisposed())



{

throw new ObjectDisposedException(GetType().FullName);

}

}
// Наследниците на този клас предефинират следните методи



// като по този начин "попълват" шаблона на метода ни

// Dispose(). Предоставили сме имплементация по подразбиране

protected virtual void DisposeManagedResources() {}

protected virtual void DisposeUnmanagedResources() {}

}


Въпреки, че кодът съдържа подробни коментари, сме длъжни да направим няколко разяснения, особено заради читателите, които нямат опит с многонишкови приложения.

  • Унищожаването на ресурса е поместено в един общ метод (DisposeImpl()), който се извиква както от финализатора, така и от метода Dispose(). Методът DisposeImpl() приема като параме­тър флаг, указващ дали се извиква по време на финализация или изрично от потребителския код.

  • Статичният метод CompareExchange() на класа Interlocked е атомарна операция12 (т.е. такава, при която е гарантирано изпълнението й само от една нишка, независимо от броя на нишките и процесорите в дадена система), която извършва следните действия:

    • Проверява дали стойността в първия аргумент е равна на третия аргумент

    • Ако стойността е равна, присвоява на първия аргумент стойността на втория аргумент

    • Връща първоначалната стойност на първия аргумент

В случая кодът:

if (Interlocked.CompareExchange(ref disposed,

TRUE, FALSE) == TRUE)

{

return;


}

е еквивалентен на следния код при приложение без поддръжка на много нишки:

if (disposed)

{

return;



}

disposed = true;



  • Влизането в първия try…finally блок е задължително за да се предпазим от прекъсване на нишката от извикването на методите Thread.Abort или Thread.Interrupt13

Обвиване на управляван ресурс – пример


За да оцените лекотата на използване на класа ResourceWrapperBase ще го приложим, за да обвием манипулатор на динамична библиотека и икона в следващия пример:

class WindowsLibrary : ResourceWrapperBase

{

private IntPtr mLibraryHandle = IntPtr.Zero;


public WindowsLibrary(string aFileName)

{

mLibraryHandle = LoadLibrary(aFileName);



Console.WriteLine("Library {0} loaded.", aFileName);

}
public IntPtr Handle

{

get


{

return mLibraryHandle;

}

}
protected override void DisposeUnmanagedResources()



{

if (mLibraryHandle != IntPtr.Zero)

{

FreeLibrary(mLibraryHandle);



mLibraryHandle = IntPtr.Zero;

Console.WriteLine("Library unloaded.");

}

base.DisposeUnmanagedResources();



}
[DllImport("kernel32.dll", SetLastError=true)]

static extern IntPtr LoadLibrary(string lpFileName);


[DllImport("kernel32.dll", SetLastError=true)]

static extern int FreeLibrary(IntPtr hModule);

}
class WindowsIcon : ResourceWrapperBase

{

private Icon mIcon;


public WindowsIcon(string aFile, int aIconId)

{

using (WindowsLibrary lib = new WindowsLibrary(aFile))



{

IntPtr hIcon = LoadIcon(lib.Handle, aIconId);

mIcon = Icon.FromHandle(hIcon);

}

Console.WriteLine("Icon {0} loaded from library {1}.",



aIconId, aFileName);

}
protected override void DisposeManagedResources()

{

if (mIcon != null)



{

mIcon.Dispose();

Console.WriteLine("Icon disposed.");

}

}


public Icon Icon

{

get



{

return mIcon;

}

}
[DllImport("user32.dll", SetLastError=true)]



public static extern IntPtr LoadIcon(IntPtr hInstance,

int lpIconId);

}
class IDisposableDemo

{

static void Main()



{

const int FIRST_EXPLORER_ICON = 101;

WindowsIcon explorerIcon =

new WindowsIcon("explorer.exe", FIRST_EXPLORER_ICON);

using (explorerIcon)

{

using (Form form = new Form())



{

form.Text = "IDisposable Demo";

form.Icon = explorerIcon.Icon;

form.ShowDialog();

}

}

Console.WriteLine("End of Main() method.");



}

}


При изпълнение на горния пример се получава сления резултат:

Вижда се, че примерът зарежда и използва неуправляваните ресурси „Windows библиотека” и „Windows икона” и ги освобождава експлицитно чрез using конструкцията. Благодарение на наследяването на класа ResourceWrapperBase дори ако тези ресурси не се освобождаваха експли­цитно, те щяха да бъдат освободени от финализаторите на съответ­ните им обвиващи класове.


Close() и експлицитна имплементация на IDisposable


В някои приложни области едни термини “звучат по-добре” от по-обобще­ните такива. Файловете, потоците и връзките към бази данни ги затваряме (Close), заключванията (lock) ги освобождаваме (Release) и т.н.

В едни от най-ползваните асемблита в .NET Framework (System.dll, System.Data.dll, System.Drawing.dll, System.WindowsForms.dll и System.Web.dll) има общо 242 класове и/или структури, имплементиращи IDisposable от които 32 го имплементират експлицитно (става въпрос за версия 1.1 на .NET Framework).

Последните извикват (невидимия от Visual Studio IntelliSense метод) Dispose() от публичния си метод Close().

Пример:


public class File : IDisposable

{

// Експлицитна имплементация на интерфейса IDisposable



void IDisposable.Dispose()

{

// ...



}
// Преобразуването към IDisposable е наложително

public void Close()

{

((IDisposable)this).Dispose();



}

}


Класове като File могат да бъдат унищожени надеждно по два начина – като се обвие използването на обекта в try...finally блок или като се използва оператора using (в C#). На практика, болшинството от C# програмистите привикват към и предпочитат оператора using, ето защо написването на подобен на Close() метод от ваша страна лично ние считаме за излишно.

Кога да извикваме IDisposable.Dispose()?


На въпроса за освобождаването на кои ресурси трябва да се грижим ръчно чрез извикване на IDisposable.Dispose() няма прост отговор. Правилото е, че ръчно трябва да се освобождават всички класове, които обвиват неуправляван ресурс и имплементират IDisposable.

Например класът FileStream имплементира IDisposable и обвива неуп­равлявания ресурс “файл”. Следователно той трябва да се освобождава ръчно чрез using конструкцията или чрез try finally блок. Класът StringReader също имплементира IDisposable, но няма нужда да бъде освобождаван ръчно, защото не държи в себе си неуправляван ресурс.

Когато ползвате даден клас от .NET Framework или от друга библиотека, трябва да проверите дали той имплементира IDisposable и да помислите дали той обвива неуправляван ресурс. Ако се съмнявате, освобождавайте ресурса ръчно. Това няма да навреди.




Сподели с приятели:
1   2   3   4   5   6   7




©obuch.info 2024
отнасят до администрацията

    Начална страница