Rust - новый язык программирования: Часть 6. Управление памятью: общие принципы, модель памяти, концепция владения

Comments

В предыдущих статьях цикла был сделан общий обзор языка программирования Rust, а также обсуждались основы синтаксиса: были описаны простейшие компоненты (переменные, простые типы данных, операторы и расширения), управляющие конструкции и составные структуры данных, а также функции и деструкторы. Дальнейшее описание Rust не представляется возможным без подробного и тщательного изучения теоретических и практических аспектов управления памятью, чему и будут посвящены несколько следующих статей цикла, начиная с данной статьи.

Следует отметить весьма важное событие: 3 июля 2013 года была представлена очередная версия Rust 0.7, содержащая значительное количество существенных изменений и усовершенствований. Поэтому информация, излагаемая в этой и последующих статьях, будет актуальной именно для версии 0.7.

Если рассматривать Rust с обобщённой точки зрения, как единую программную среду, то становится очевидным (и авторы языка подтверждают это), что модель памяти построена в тесной связи с концепцией параллельно выполняемых задач (tasks). Для упрощения восприятия можно считать параллелизм задач своего рода "ядром", вокруг которого сформирована система управления памятью. Поэтому при описании концепции управления памятью и её реализации неизбежно будут затрагиваться вопросы параллельного выполнения задач. В наиболее кратком предварительном изложении эти взаимосвязи можно сформулировать следующим образом: управление памятью обеспечивает корректное параллельное выполнение задач, а механизмы изоляции параллельных задач и обмена данными между ними работают исключительно благодаря концепциям владения и времени существования, реализованным в модели памяти Rust.

1. Модель памяти

Память любой rust-программы состоит из набора статических (static) элементов, из некоторого количества задач (tasks), каждая из которых обладает собственным локальным стеком (stack), и из общей памяти, так называемой "кучи" (heap). Неизменяемые области общей памяти могут совместно использоваться задачами, то есть, быть разделяемыми (shared). Изменяемые области общей памяти не могут быть совместно используемыми.

Память в стеке задачи выделяется в форме слотов (slots), а в общей памяти выделяемые фрагменты обозначаются, как boxes (значение этого специфического для Rust термина будет объяснено в следующем разделе).

1.1. Распределение памяти и время существования

Статические элементы (static items) программы — это те функции, модули и типы, значения которых вычисляются во время компиляции и хранятся отдельно в образе памяти rust-процесса. Очевидно, что статические элементы никогда не создаются динамически и никогда не освобождаются динамически.

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

Общая память (heap)— это объединяющий термин, который описывает два различных набора элементов, которые в Rust называются boxes. Сразу следует уточнить, что авторы языка классифицируют box, как "ссылку на область в общей памяти, содержащую другое значение", но с точки зрения семантики сущность box обладает своеобразным дуализмом — это и сам блок в общей памяти, и связанный с ним указатель (без которого какое-либо использование блока памяти невозможно). В каждом конкретном случае смысловой акцент должен уточняться по контексту, при этом оба смысловых значения не противопоставляются, а органично дополняют друг друга. Таким образом, более разумно трактовать термин "box", как блок памяти, выделенный из общей памяти (heap), и связанный с ним указатель, но не простой (как в C), а так называемый "умный указатель" (smart pointer), подобный тем, которые используются в других языках программирования. Указатели на блок памяти могут быть двух видов: управляемые (managed boxes), за которые, вообще говоря, отвечает механизм сборки мусора (garbage collection), и собственные (owned boxes), аналогом которых можно считать unique pointers, основная задача которых заключается в обеспечении существования только одного владельца у данного блока памяти. Время существования (lifetime) выделенного блока в общей памяти зависит от времени существования указателей на этот блок. Поскольку указатели могут сами по себе быть переданы во фреймы и из фреймов (в частности, в функции или в блоки кода или из функций или из блоков кода), или сами сохранены в памяти, то блоки, выделенные в общей памяти задачи (heap), могут существовать дольше, чем фрейм (функция или блок кода), в котором они были выделены.

1.2. Общая концепция владения

В Rust общая концепция владения объектом (object ownership) формализована и реализована посредством передачи управления временем существования объекта либо переменной, связываемой с этом объектом, либо сборщику мусора, локальному в рамках текущей задачи. Таким образом, только владелец объекта управляет его временем существования, вызывая в нужный момент деструктор, и определяет, является ли данный объект изменяемым.

Владение рекурсивно, поэтому свойство изменяемости наследуется также рекурсивно, соответственно и деструктор выполняет удаление всего дерева объектов, принадлежащих владельцу. Переменные представляют собой владельцев самого высокого уровня, и при выходе из области видимости (scope) переменная уничтожает связанный с ней объект, владельцем которого она является. "Умный указатель" на блок в памяти задачи (box), управляемый сборщиком мусора, формирует новое дерево владения, а деструктор вызывается, когда начинает работать механизм сборки мусора.

Например, показанная ниже структура владеет объектами, содержащимися в полях x и y:

struct Foo { x: int, y: ~int }

Далее переменная a становится владельцем структуры Foo, а следовательно, и владельцем полей этой структуры:

{ let a = Foo { x: 5, y: ~10 }; }

При выходе из области видимости (в данном случае: из блока кода) переменная a перестаёт существовать, и автоматически вызывается деструктор для поля ~int в данной структуре.

После этого определяется изменяемая переменная b, и свойство изменяемости рекурсивно наследуется всеми объектами, которыми владеет эта переменная:

let mut b = Foo { x: 5, y: ~10 };
b.x = 10;

Если объект не содержит указателей на блоки памяти, управляемых локальным сборщиком мусора, то он состоит из единственного дерева владения, и ему можно присвоить свойство (trait; признак)Owned, которое позволяет пересылать такой объект между задачами. Только для типов, обладающих свойством Owned, можно написать непосредственную реализацию специализированных деструкторов; тем не менее box-значения, управляемые сборщиком мусора, могут включать в себя типы со специализированными деструкторами.

1.3. Владение памятью

Теперь следует от общей концепции владения, описанной в предыдущем разделе, перейти к рассмотрению более частной концепции владения памятью. Считается, что любая задача владеет всей памятью, к которой она имеет безопасный доступ через локальные переменные, а также через управляемые (managed), собственные (owned) и заимствованные (borrowed) указатели (о них речь пойдёт в следующих статьях).

Как уже было сказано ранее, при выходе из фрейма стека все локально созданные в нём объекты освобождаются и все указатели на блоки памяти, как управляемые, так и собственные, уничтожаются. В тех случаях, когда управляемый указатель на блок памяти представляет рекурсивную изменяемую управляемую структуру, он может быть циклическим, и освобождение памяти, выделенной для такой структуры может быть отложено до того момента, когда локальный для данной задачи сборщик мусора начнёт свою работу. Впрочем, при написании кода можно избавиться от таких "временно некорректных" объектов, ограничившись использованием только собственных указателей на блоки памяти (owned boxes) и прочих типов данных, которые не управляются механизмом сборки мусора.

После завершения задачи её стек полностью очищается в обязательном порядке, таким образом, уничтожаются абсолютно все существовавшие указатели на блоки общей памяти, а сама общая память, выделенная этой задаче, немедленно освобождается.

1.4. Слоты памяти

В начале статьи уже отмечалось, что стек каждой задачи содержит слоты (slots), которые являются компонентами фрейма стека и представляют собой либо параметры функции, либо временные объекты, либо локальные переменные.

Локальные переменные, которые иногда обозначают термином "элемент локального стека" (stack-local allocation), содержат собственно значение, размещённое в области памяти стека. Очевидно, что это значение является составной частью фрейма стека. Локальные переменные и параметры функций по умолчанию неизменяемы. Ключевое слово mut, явно указанное при объявлении, предоставляет возможность изменения переменных и параметров. Несмотря на то, что при размещении в стеке локальные переменные не инициализируются, их использование становится возможным только после явной инициализации, за этим строго следит компилятор.

Заключение

Управление памятью в Rust представляет собой отдельную и довольно-таки обширную тему для обсуждения, поэтому более подробное рассмотрение управляемых и собственных "умных указателей" на блоки памяти, а также особого типа указателей —заимствованных указателей (borrowed pointers) будет продолжено в следующих статьях цикла.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=947155
ArticleTitle=Rust - новый язык программирования: Часть 6. Управление памятью: общие принципы, модель памяти, концепция владения
publish-date=01032013