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



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

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

Автори


Стоян Дамов и Димитър Бонев

Необходими знания


  • Базови познания за .NET Framework и CLR

  • Базови познания за общата система от типове в .NET (Common Type System)

  • Базови познания за езика C#

  • Незадължителни базови познания за езиците C и C++







Приключение. Ха-ха! Силни усещания. Ха-ха! Джедаят не копнее за такива неща. Йода

Съдържание


  • Управление на паметта при различните езици и платформи

  • Управление на паметта в .NET Framework

  • Как се заделя памет в .NET?

  • Как работи системата за почистване на паметта?

  • Поколения памет

  • Блок памет за големи обекти

  • Финализацията на обекти в .NET

  • Деструкторите в C#. Опашката Freachable

  • Тъмната страна на финализацията

  • Съживяване на обекти

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

  • Базов клас, обвиващ неуправляван ресурс

  • Close() и изрична имплементация на IDisposable

  • Взаимодействие със системата за почистване на паметта

  • Слаби референции

  • Ефективно използване на паметта

  • Техниката “пулинг на ресурси”

В тази тема...


В настоящата тема ще научите как да пишете правилен и ефективен код по отношение използването на паметта и ресурсите в .NET Framework. Ще започнем със сравнение на предимствата и недостатъците на ръчното и автоматично им управление. След това ще разгледаме по-обстойно автоматичното управление, фокусирайки се най-вече върху системата за почистване на паметта в .NET (т. нар. garbage collector). Ще обърнем по-голямо внимание на взаимодействието на вашия код с нея и практиките, с които можете да й помогнете да работи възможно най-ефективно.

Ако решите да прескочите тази тема, понеже на пръв поглед изглежда скучна, недейте! Тя е изпъстрена с примери за това какъв код да избягвате да пишете и какъв код да се стремите да пишете. В нея ще намерите имплементация на:



  • Базов клас за многократна употреба, обвиващ неуправляван ресурс, който имплементира правилно интерфейса IDisposable и има финализатор, а също така е и безопасен за употреба в многонишкови програми (thread-safe)

  • Клас за многократна употреба, имплементиращ thread-safe пул от ресурси

Управление на паметта при различните езици и платформи


В почти всички езици за програмиране, заделянето на динамична памет се извършва “ръчно”, т.е. се обявява изрично от програмиста. Разликата е в освобождаването и съществуват три начина за освобождаване на памет1:

  • Ръчно – паметта се освобождава изрично от програмиста, например в C и C++.

  • Автоматично – паметта се освобождава от система за автоматично почистване на паметта (често наричана система за почистване на боклук, Garbage Collector, GC), която обикновено се задейства автоматично при недостиг на памет. Такъв подход се използва например в езици като Java, при които кодът се изпълнява от виртуална машина, или като при повечето .NET езици, където кодът се изпълнява в контролирана среда, осигуряваща коректно изпълне­ние на кода (.NET CLR).

  • Смесено – паметта може да се освобождава както директно от програмиста, така и автоматично от система за почистване на боклук, например Visual Basic (версиите преди VB.NET).

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

Първо ще разгледаме ръчното управление на памет, като посочим начини­те за заделяне и освобождаване на памет и ресурси в два известни езика от по-ниско ниво – C и C++. След това ще разгледаме автоматичното управление в .NET и ще сравним възможно най-обективно двата начина за управление на паметта и ресурсите за да имате ясна представа какво можете и какво не можете да направите във вашите настоящи и бъдещи .NET приложения.


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





Страхът е пътят към тъмната страна. Страхът води до използване на управлявани езици. Управляваните езици водят до използването на Garbage Collector. Garbage Collector-ът води до страдания. C/C++ Йода

Управление на паметта в езика C


Заделянето и освобождаването на памет в езика C се прави ръчно от програмиста посредством библиотечните функции malloc(), realloc() и free()2. Функцията malloc() заделя блок последователни байтове от динамичната памет (т. нар. heap) и връща указател към първия байт от тази памет, free() я освобождава, а realloc() може да заделя, освобождава, разширява и премества блокове памет и е своеобразен менажер на паметта (memory manager), чрез който могат да се имплементират malloc() и free().

Управление на паметта в езика C++


В езика C++, освен гореизброените функции можете (и се препоръчва) да използвате вградените в езика двойка оператори new и delete3 (които в повечето случаи са имплементирани посредством malloc() и free()). Предимството на оператора new пред функцията malloc() е, че след като задели памет за инстанция на даден тип, операторът извиква код за инициализация на типа (наричан конструктор) в тази памет. Операторът delete извиква код за разрушаване на инстанцията (наричан деструктор), след което освобождава паметта, заета от оператора new4.

Деструкторите в C++


Деструкторът на инстанция на определен тип (обект) се изпълнява, когато:

  • се извика ръчно в кода

  • обектът напусне обхвата (scope), в който е създаден или

  • при възникване на изключение.

Създаване на обекти в C++


Създаването на обект в динамичната памет с малки изключения5 става с помощта на вградения оператор new. В най-общи линии, той се опитва да задели памет, достатъчна за да помести инстанция на подадения тип, след което, ако успее, извиква конструктора за да инициализира обекта в тази памет. Ако не успее, в зависимост от няколко условия, които няма да разглеждаме, или изхвърля изключение или записва нулева (NULL) стой­ност в указателя. Обект, заделен в динамичната памет, се разрушава с вградения оператор delete, който извиква деструктора на обекта и ако не възникне изключение, освобождава заделената памет от оператора new.

Автоматично унищожаване на ресурси в C++


Фактът, че деструкторът на обект, инстанциран в стека, се извиква автоматично при напускането на неговия обхват или при възникване на изключение (и в частност техниката RAII6), е може би най-важната причина C++ да не се нуждае от клаузата finally, без която не е възможно да се пише код, устойчив на изключения, в езици като C# и Java7. Наличието на деструктори в C++ прави възможно автоматичното освобождаване на всички видове ресурси, например:

// след напускане на обхвата на mem паметта ще бъде освободена

boost::shared_ptr mem(new char[20]);

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

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



Освобождаването на паметта и ресурсите в C е възможно да се прави само ръчно (освен когато се ползва Garbage Collector за C).

Предимства и недостатъци на ръчното управление на паметта и ресурсите


Ще изброим предимствата и недостатъците, тъй като представянето им в табличен вид е неудобно за четене.

Предимства на ръчното управление


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

  • Възможно е освобождаване на ресурсите в известен, желан момент (например извикването на free или delete или автоматичното извикване на деструктор в C++ ръчно, при излизане на обекта извън обхват или при възникване на изключение).

  • Имаме пълен контрол на начина за заделяне и освобождаване на памет, включително написването и замяната на мениджъра на паметта, както и предефинирането на заделяне и освобождаване на памет за желан от нас потребителски тип (в C++).

  • Възможно е конструиране на обект на зададен от нас адрес (полезно в C++ за писане на устойчив на изключения код, както и в C при писане на системен код).

  • Възможно е заделяне на блок памет от стека (с помощта на alloca() или използването на масиви с променлив размер - C99).

  • Липсата на памет може да се установи по няколко начина и да се предприеме някакво действие.

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


Недостатъците са повече от споменатите по-долу и се надяваме, че изброените проблеми ще ви дадат достатъчно основание да разберете значимостта на системата за автоматично почистване на паметта (GC) в .NET Framework. Да започнем с най-често срещаните грешки, които водят до проблеми и се допускат дори от най-опитните програмисти:

  • Несъответствие в броя на заделянията и освобождаванията, което води до “изтичане” на памет (memory leak). Недостатъкът очевидно е ръчното освобождаване на памет. Като частен случай трябва да посочим и изтичане на памет, породено от недобре написан конструктор на клас по отношение на възникването на изключения.

  • Несъответствие в извикването на операторите за типове и масиви от типове, например извикване на delete, за памет, заделена с new[].

  • Опит за четене или писане на вече освободена памет или опит за повторно освобождаване на памет.

  • Опит за писане в незаделена от програмиста памет на валиден адрес в адресното пространство на вашата програма или запис на повече информация от заделената за това памет – проблем, допринесъл за най-големите пробиви свързани със сигурността.

Не можем да не бъдем честни към C/C++ програмистите и да споменем, че за повечето от горепосочените проблеми съществува решение – вдигане на нивото на предупреждения от компилатора, използването на assertions, т. нар. умни указатели (smart pointers), STL контейнери, мощни библиоте­ки като boost и техниката RAII.

Ето и няколко недостатъка, за които също съществуват решения, но в повечето случаи те са свързани с допълнителни разходи:



  • Бавно заделяне (и освобождаване) на динамична памет, особено с мениджъра на паметта по подразбиране.

  • Фрагментиране на динамичната памет поради неоптимизирана реа­лизация на мениджъра на паметта по подразбиране.

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

  • Цяла глава може да се посвети на една от най-трудните и податливи на грешки задачи в C++ - броене на референциите към обектите, най-вече при йерархии от обекти, където има циклични референции. При такива проблеми, написването на правилен код с броене на референции граничи с героизъм.

Трябва да отбележим, че съществуват свободни (при това доста добри) имплементации на Garbage Collector за C и C++, които обаче не са широко разпространени и използвани.

Изброените недостатъци по-горе са довели до създаването на скъпи продукти като Insure++, Rational Purify и CompuWare BoundsChecker, които да се справят с куп проблеми, които както ще се убедите сами, просто не съществуват в .NET.





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




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

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