Содержание


Rust - новый язык программирования: Часть 7. Работа с памятью - собственные и управляемые блоки памяти

Comments

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

Следует ещё раз отметить, что одной из особенностей, выделяющей Rust среди других языков программирования, является безопасное ручное управление памятью (safe manual memory management). Это нужно понимать так, что программисту предоставляется достаточно возможностей для гибкого и эффективного управления распределением памяти и в то же время обеспечивается безопасность использования выделенной памяти. Чтобы лучше понять, в чём именно состоит особенность управления памятью в Rust, можно провести краткое сравнение с другими языками.

В C и C++, например, управление памятью полностью ручное. При всех очевидных достоинствах этой методики не менее очевидным является ее главный недостаток: за безопасное использование памяти отвечает только программист. Следствием этого является то, что невыявленные вовремя нарушения безопасности памяти могут привести к падениям программ в лучшем случае и к взлому с использованием этих уязвимостей в системе памяти в худшем случае.

Большинство современных языков программирования используют только механизм сборки мусора для управления распределяемой памятью (Java, Go, JavaScript, Ruby, Haskell и другие). С безопасностью здесь всё в порядке - ошибки при работе с памятью выявляются на этапе компиляции, но и здесь имеется существенный недостаток: механизм сборки мусора требует для работы достаточно много времени в процессе выполнения приложения, тем самым снижая производительность. Кроме того, в некоторых реализациях механизм сборки мусора может активизироваться в совершенно неудобное время, фактически блокируя "отзывчивость" приложения.

В некоторых языках (например, в C#) механизм сборки мусора дополняется системой размещения данных в стеке и ссылок (параметров-ссылок, иногда локальных ссылок). Тем не менее, обычно этого недостаточно, чтобы писать программы вообще без использования механизма сборки мусора; такая система слишком проста, чтобы статически кодировать всё, что выходит за рамки большинства основных шаблонов управления памятью.

С учётом сказанного выше Rust фактически создаёт собственную отдельную категорию "безопасное ручное управление памятью" (хотя у начинающих осваивать Rust слово "ручное" может вызывать возражения). Он расширяет и усовершенствует последнюю описанную выше систему "сборка мусора в сочетании с типами данных и ссылками" по двум важным основным направлениям:

  1. Возможность выделять память таким образом, что она не будет отслеживаться механизмом сборки мусора, и освобождать её вручную, если это действительно необходимо. Эта возможность в Rust называется "уникальные указатели" ("unique pointers"). Rust будет автоматически освобождать память, у которой только один единственный владелец со своим собственным указателем (owned pointer), сразу после того, как этот собственный указтель выйдет из области видимости. Кроме того, нетрудно написать функцию, действующую в точности так же, как free(), так что программист может абсолютно точно выбрать время уничтожения своих объектов. Уникальные указатели не отслеживаются механизмом GC (если только они не указывают на тип, который транзитивно содержит указатель, управляемый GC), поэтому являются эффективным способом избавления от задержек, связанных с GC-маркировкой.
  2. Возможность возвращать ссылки и помещать ссылки внутрь структур данных. Эти ссылки также не отслеживаются механизмом сборки мусора. Пока такие ссылки находятся под управлением стека и соблюдают его правила (stack discipline), это означает, что они указывают на память, которая была выделена одной из вызывающих сторон текущей функции, так что компилятор позволяет размещать их в любом месте. Это добавляет изрядную долю выразительности к методике ссылок-параметров (reference parameter), и позволяет писать большое количество программ, в которых механизм сборки мусора не используется вообще.

В отношении безопасности и производительности система "безопасного ручного управления памятью" поддерживает наиболее разумный баланс. Разработчик получает в своё распоряжение защиту памяти, как в языках с механизмом сборки мусора, и в то же время - средства управления, похожие на небезопасное ручное управление памятью. Конечно, и такая система не лишена недостатков и, что более важно, сложна в реализации и организации программного интерфейса. Изучение применения ссылок и уникальных указателей на первых порах достаточно трудно. Но после того, как эта система освоена, сразу становится очевидной её неимоверная гибкость в сочетании с необходимой безопасностью и производительностью.

1. Блоки памяти (boxes) и указатели на них

По умолчанию Rust размещает в стеке данные не только простых, но и составных типов. Другими словами, это означает, что в Rust составные типы по умолчанию не являются указателями на блоки памяти (unboxed). То есть, инструкция

let x = Point { x: 1f, y: 1f };

создаёт указанную структуру в стеке. При копировании выполняется копирование всей структуры (её содержимого) в целом, а не указателя на неё.

Это хорошо работает для небольших структур, подобных вышеупомянутой Point, и даже зачастую в этом случае более эффективно, нежели операция выделения памяти и назначение указателя на неё. Но для больших структур и для объектов, состояние которых объявлено изменяемым, всё же может оказаться более удобным наличие единственной копии в стеке или в общей памяти и обращение к ней через указатель.

В предыдущей статье уже было сказано, что термин box определяется, как указатель на блок общей памяти (heap), содержащей некоторое значение, а также отмечен некоторый дуализм этой сущности, вследствие чего точный смысл, как правило, определяется из контекста. Во многих случаях box удобнее считать именно указателем на блок общей памяти.

Существуют два типа таких указателей: собственный указатель на блок общей памяти (owned box), обозначаемый префиксом "тильда" (~), и управляемый указатель на блок общей памяти (managed box), обозначаемый префиксом "коммерческое at" (@).

2. Собственные указатели на блоки общей памяти

Собственный указатель на блок общей памяти (~) определяет конкретную область общей памяти, принадлежащую одному и только одному владельцу. Выделенный блок памяти наследует свойство изменяемости и время существования от своего владельца, как показано в листинге 1.

Листинг 1. Определение неизменяемого и изменяемого указателей на блок памяти
let x = ~5;  // неизменяемый блок памяти выделен
let mut y = ~5;  // здесь блок памяти наследует изменяемость от своего владельца
*y += 2;  // оператор * необходим для доступа к содержимому данного блока памяти

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

Для наглядности в листинге 2 показана структура, которая не может быть скомпилирована из-за того, что отсутствие уровня косвенности в данном случае может привести к потенциальной бесконечности размера этой структуры.

Листинг 2. Некорректно определённая рекурсивная структура
struct RecStru {
  child: Option<RecStru>
}

Здесь следует пояснить, что тип Option является перечислением, представляющим необязательное, опциональное значение. Оно сравнимо с указателем, который может иметь null-значение, во многих других языках, но при этом необходимо учитывать, что Option хранит своё значение не в выделенном блоке общей памяти (то есть, это unboxed-значение).

Чтобы устранить ошибку при компиляции, необходимо добавить уровень косвенности с помощью собственного указателя на блок памяти, позволяющего разместить член данной структуры child в общей памяти, то есть, вне пределов этой структуры, обеспечивая таким образом конечность её размера, как показано в листинге 3.

Листинг 3. Корректное определение рекурсивной структуры
struct RecStru {
  child: Option<~RecStru>
}

Любое значение собственного блока общей памяти связано соотношением "один-к-одному" с местоположением этого блока в памяти. Копирование значения собственного блока памяти создаёт так называемую глубокую (или полную) копию (deep copy) содержимого этого блока и как результат возвращает указатель на новый блок памяти. Например, создание переменной с типом "собственный указатель на блок памяти" и присваивание ей значения этого же типа может выглядеть следующим образом:

let x: ~int = ~10;

3. Управляемые указатели на блоки общей памяти

Управляемый указатель на блок общей памяти (@) определяет конкретную область общей памяти, время существования которой управляется сборщиком мусора (garbage collector), локальным для текущей задачи (task-local). Такой блок памяти уничтожается (освобождается) в некоторый момент после того, как не останется ни одной ссылки на него, но не позднее момента завершения самой задачи. У управляемых указателей на блоки памяти нет владельца, поэтому они сами создают новое дерево владения и не имеют возможности наследовать свойство изменяемости. Управляемый блок памяти сам владеет содержащимся в нём объектом, а его изменяемость определяется типом, заданным при определении (@ или @mut). Любой объект, содержащий управляемый указатель на блок общей памяти, не является собственным (Owned), следовательно, не может пересылаться между задачами. В листинге 4 показаны варианты определения управляемых указателей на блоки памяти.

Листинг 4. Допустимые варианты определения управляемых указателей на блоки общей памяти
let a = @5;  // неизменяемый управляемый блок памяти выделен
let mut b = @5;  // переменная изменяемая, но блок памяти неизменяемый
b = @10;         // поэтому возможно связывание с другим неизменяемым блоком памяти

let c = @mut 5;  // переменная неизменяемая, но блок памяти изменяемый
*c = 10;         // поэтому возможно косвенное обращение через переменную 
                  //и изменение блока памяти

let mut d = @mut 5; // изменяемые и переменная, и блок памяти
*d += 5;            // поэтому возможны оба способа изменения содержимого блока памяти:
d = @mut 15;        // и косвенный, и прямой

Изменяемая и неизменяемая переменные могут одновременно ссылаться на один и тот же блок общей памяти при условии, что их типы совместимы. Но изменяемость самого блока памяти при этом продолжает оставаться свойством его собственного типа (именно как указателя на блок общей памяти). Поэтому изменяемая ссылка на неизменяемый блок памяти не может быть присвоена указателю на изменяемый блок памяти. Примеры допустимых и некорректных присваиваний показаны в листинге 5.

Листинг 5. Варианты присваивания указателей на изменяемые и неизменяемые блоки общей памяти
let a = @1;  // выделен неизменяемый блок памяти
let b = @mut 2;  // выделен изменяемый блок памяти

let mut c : @int;  // объявлена переменная типа "управляемый неизменяемый int"
let mut d : @mut int;  // объявлена переменная типа "управляемый изменяемый int"

c = a;  // корректная операция: типы полностью совпадают
d = b;  // корректная операция: типы полностью совпадают
c = b;  // ошибка: несовпадение свойства изменяемости типов
d = a;  // ошибка: несовпадение свойства изменяемости типов

Несколько переменных типа "указатель на управляемый блок памяти" могут указывать на одну и ту же область в общей памяти, а при копировании такого объекта создаётся так называемая неглубокая копия (shallow copy) указателя, и если управляемый блок памяти реализован с использованием механизма счётчика ссылок (reference-counting), то счётчик ссылок для данного объекта увеличивается соответственно. Вот пример создания переменной типа "управляемый указатель на блок памяти" и присваивания ей значения того же типа:

let x: @int = @10;

Некоторые операции (скажем, выбор поля) выполняют неявное разыменование (implicit dereference) указателей на блоки памяти. В листинге 6 приведён пример такого неявного разыменования при доступе к значению блока памяти.

Листинг 6. Неявное разыменование указателя при обращении к значению блока памяти
struct SimpleStru { y: int }
let x = @SimpleStru{ y: 10 };
assert!( x.y == 10 );

Другие операции работают со значением указателя на блок памяти как с адресом, имеющим размер одного машинного слова (в текущей реализации). При выполнении таких операций для доступа к значению, содержащемуся в блоке памяти, требуется явное разыменование указателя на данный блок памяти. Явное разыменование (explicitly dereferencing) обозначается унарным оператором "звёздочка" (*). Примерами операций, требующих явного разыменования указателя на блок памяти, могут служить копирование значений блоков памяти (x = y) и передача значений блоков памяти в функции в качестве параметров (f(x,y)). В листинге 7 показан один из вариантов использования явного разыменования.

Листинг 7. Пример применения явного разыменования при передаче параметра в функцию
fn takes_boxed( b: @int ) {
  println( b.to_str() );
}

fn takes_unboxed( b: int ) {
  println( b.to_str() );
}

fn main() {
  let x: @int = @10;
  takes_boxed( x );
  takes_unboxed( *x );
}

4. Замечание о семантике передачи значения блока памяти

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

Листинг 8. Перемещение значения собственного блока памяти
let x = ~5;
let y = x.clone();  // для y выделяется новый собственный блок памяти
let z = x;  // новый блок памяти не выделяется, блок памяти x передаётся z,
            // и после этого x уже не может быть использован

Ранее уже было отмечено, что изменяемость каждого собственного блока памяти является свойством его владельца, а не самого блока, поэтому после передачи (перемещения) блока памяти он может из неизменяемого стать изменяемым и наоборот, как показано в листинге 9.

Листинг 9. Изменение свойства изменяемости блока памяти при его передаче
let r = ~13;
let mut s = r;  // блок становится изменяемым
*s += 1;  // допустимая операция, в блоке хранится значение 14
let t = s;  // блок становится неизменяемым

Заключение

В данной статье рассматривались собственные и управляемые блоки памяти и указатели на них, а также простые для понимания примеры их использования. Но более глубокое понимание системы безопасного ручного управления памятью в Rust приходит после освоения заимствованных указателей и использования их в сочетаниях с собственными и управляемыми указателями. О заиствованных указателях речь пойдёт в следующих двух статьях цикла.


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


Похожие темы


Комментарии

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

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