ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1164
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
методов, и работает только для типов, реализующих признак
Deref
. Это происходит автоматически, когда мы передаём в качестве аргумента функции или метода ссылку на значение определённого типа, которое не соответствует типу параметра в определении функции или метода. В результате серии вызовов метода deref тип, который мы передали, преобразуется в тип, необходимый для параметра.
Разыменованное приведение было добавлено в Rust, так что программистам, пишущим вызовы функций и методов, не нужно добавлять множество явных ссылок и разыменований с помощью использования
&
и
*
. Функциональность разыменованного приведения также позволяет писать больше кода, который может работать как с ссылками, так и с умными указателями.
Чтобы увидеть разыменованное приведение в действии, давайте воспользуемся типом
MyBox
определённым в листинге 15-8, а также реализацию
Deref добавленную в листинге 15-10. Листинг 15-11 показывает определение функции, у которой есть параметр типа срез строки:
Файл: src/main.rs
Deref
. Это происходит автоматически, когда мы передаём в качестве аргумента функции или метода ссылку на значение определённого типа, которое не соответствует типу параметра в определении функции или метода. В результате серии вызовов метода deref тип, который мы передали, преобразуется в тип, необходимый для параметра.
Разыменованное приведение было добавлено в Rust, так что программистам, пишущим вызовы функций и методов, не нужно добавлять множество явных ссылок и разыменований с помощью использования
&
и
*
. Функциональность разыменованного приведения также позволяет писать больше кода, который может работать как с ссылками, так и с умными указателями.
Чтобы увидеть разыменованное приведение в действии, давайте воспользуемся типом
MyBox
определённым в листинге 15-8, а также реализацию
Deref добавленную в листинге 15-10. Листинг 15-11 показывает определение функции, у которой есть параметр типа срез строки:
Файл: src/main.rs
1 ... 35 36 37 38 39 40 41 42 ... 62
Листинг 15-11: Функция
hello
имеющая параметр
name
типа
&str
Можно вызвать функцию hello со срезом строки в качестве аргумента, например hello("Rust");
. Разыменованное приведение делает возможным вызов hello со ссылкой на значение типа
MyBox
, как показано в листинге 15-12.
Файл: src/main.rs
Листинг 15-12: Вызов
hello
со ссылкой на значение
MyBox
, которое работает из-за
разыменованного приведения
Здесь мы вызываем функцию hello с аргументом
&m
, который является ссылкой на значение
MyBox
. Поскольку мы реализовали типаж
Deref для
MyBox
в листинге 15-10, то Rust может преобразовать
&MyBox
в
&String вызывая deref
Стандартная библиотека предоставляет реализацию типажа
Deref для типа
String
,
которая возвращает срез строки, это описано в документации API типажа
Deref
. Rust снова вызывает deref
, чтобы превратить
&String в
&str
, что соответствует определению функции hello fn hello
(name: &
str
) { println!
(
"Hello, {name}!"
);
} fn main
() { let m = MyBox::new(
String
::from(
"Rust"
)); hello(&m);
}
Если бы Rust не реализовал разыменованное приведение, мы должны были бы написать код в листинге 15-13 вместо кода в листинге 15-12 для вызова метода hello со значением типа
&MyBox
Файл: src/main.rs
Листинг 15-13: Код, который нам пришлось бы написать, если бы в Rust не было разыменованного
приведения ссылок
(*m)
разыменовывает
MyBox
в
String
. Затем
&
и
[...]
принимают фрагмент строки
String
, равный полной строке для соответствия сигнатуре hello
. Этот код без deref-согласований сложнее читать, писать и понимать, поскольку в нем задействованы все эти символы. Deref coercion позволяет Rust автоматически обрабатывать эти преобразования.
Когда типаж
Deref определён для задействованных типов, Rust проанализирует типы и будет использовать
Deref::deref столько раз, сколько необходимо, чтобы получить ссылку, соответствующую типу параметра. Количество раз, которое нужно вставить
Deref::deref определяется во время компиляции, поэтому использование разыменованного приведения не имеет накладных расходов во время выполнения!
Как разыменованное приведение взаимодействует с изменяемостью
Подобно тому, как вы используете типаж
Deref для переопределения оператора
*
у неизменяемых ссылок, вы можете использовать типаж
DerefMut для переопределения оператора
*
у изменяемых ссылок.
Rust выполняет разыменованное приведение, когда находит типы и реализации типажей в трёх случаях:
Из типа
&T
в тип
&U
когда верно
T: Deref
Из типа
&mut T
в тип
&mut U
когда верно
T: DerefMut
Из типа
&mut T
в тип
&U
когда верно
T: Deref
Первые два случая идентичны друг другу, за исключением того, что второй реализует изменяемость. В первом случае говорится, что если у вас есть
&T
, а
T
реализует
Deref для некоторого типа
U
, вы сможете прозрачно получить
&U
. Во втором случае говорится, что такое же разыменованное приведение происходит и для изменяемых ссылок.
fn main
() { let m = MyBox::new(
String
::from(
"Rust"
)); hello(&(*m)[..]);
}
Третий случай хитрее: Rust также приводит изменяемую ссылку к неизменяемой. Но обратное не представляется возможным: неизменяемые ссылки никогда не приводятся к изменяемым ссылкам. Из-за правил заимствования, если у вас есть изменяемая ссылка,
эта изменяемая ссылка должна быть единственной ссылкой на данные (в противном случае программа не будет компилироваться). Преобразование одной изменяемой ссылки в неизменяемую ссылку никогда не нарушит правила заимствования.
Преобразование неизменяемой ссылки в изменяемую ссылку потребует наличия только одной неизменяемой ссылки на эти данные, и правила заимствования не гарантируют этого. Следовательно, Rust не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую ссылку возможно.
Запуск кода при очистке с помощью типажа Drop
Вторым важным типажом умного указателя является Drop, который позволяет регулировать, что происходит, когда значение вот-вот выйдет из области видимости. Вы можете реализовать типаж Drop для любого типа, а также использовать этот код для высвобождения ресурсов, таких как файлы или сетевые соединения.
Мы рассматриваем
Drop в контексте умных указателей, потому что функциональность свойства
Drop практически всегда используется при реализации умного указателя.
Например, при сбросе
Box
происходит деаллокация пространства на куче, на которое указывает box.
В некоторых языках для некоторых типов программист должен вызывать код для освобождения памяти или ресурсов каждый раз, когда он завершает использование экземпляров этих типов. Примерами могут служить дескрипторы файлов, сокеты или блокировки. Если забыть об этом, система окажется перегруженной и может упасть. В
Rust вы можете указать, что определённый фрагмент кода должен выполняться всякий раз, когда значение выходит из области видимости, и компилятор автоматически будет его вставлять. Как следствие, вам не нужно заботиться о размещении кода очистки везде в программе, где завершается работа экземпляра определённого типа - утечки ресурсов все равно не будет!
Вы можете задать определённую логику, которая будет выполняться, когда значение выходит за пределы области видимости, реализовав признак
Drop
. Типаж
Drop требует от вас реализации одного метода drop
, который принимает изменяемую ссылку на self
. Чтобы увидеть, когда Rust вызывает drop
, давайте реализуем drop с помощью операторов println!
В листинге 15-14 показана структура
CustomSmartPointer
, единственной уникальной функциональностью которой является печать
Dropping CustomSmartPointer!
, когда экземпляр выходит из области видимости, чтобы показать, когда Rust выполняет функцию drop
Файл: src/main.rs
Листинг 15-14: Структура
CustomSmartPointer
, реализующая типаж
Drop
, куда мы поместим наш код
очистки
Типаж
Drop включён в прелюдию, поэтому нам не нужно вводить его в область видимости. Мы реализуем типаж
Drop для
CustomSmartPointer и реализуем метод drop
,
который будет вызывать println!
. Тело функции drop
- это место, где должна располагаться вся логика, которую вы захотите выполнять, когда экземпляр вашего типа выйдет из области видимости. Мы печатаем здесь текст, чтобы наглядно продемонстрировать, когда Rust вызовет drop
В main мы создаём два экземпляра
CustomSmartPointer и затем печатаем
CustomSmartPointers created
. В конце main наши экземпляры
CustomSmartPointer выйдут из области видимости и Rust вызовет код, который мы добавили в метод drop
,
который и напечатает наше окончательное сообщение. Обратите внимание, что нам не нужно вызывать метод drop явно.
Когда мы запустим эту программу, мы увидим следующий вывод:
Rust автоматически вызывал drop в момент выхода наших экземпляров из области видимости, тем самым выполнив заданный нами код. Переменные ликвидируются в обратном порядке их создания, поэтому d
была ликвидирована до c
. Цель этого примера - дать вам наглядное представление о том, как работает метод drop
; в struct
CustomSmartPointer
{ data:
String
,
} impl
Drop for
CustomSmartPointer { fn drop
(&
mut self
) { println!
(
"Dropping CustomSmartPointer with data `{}`!"
, self
.data);
}
} fn main
() { let c = CustomSmartPointer { data:
String
::from(
"my stuff"
),
}; let d = CustomSmartPointer { data:
String
::from(
"other stuff"
),
}; println!
(
"CustomSmartPointers created."
);
}
$
cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
типичных случаях вы будете задавать код очистки, который должен выполнить ваш тип,
а не печатать сообщение.
Раннее удаление значения с помощью std::mem::drop
К сожалению, отключение функции автоматического удаления с помощью drop является не простым. Отключение drop обычно не требуется; весь смысл типажа
Drop том, чтобы о функции позаботились автоматически. Иногда, однако, вы можете захотеть очистить значение рано. Одним из примеров является использование интеллектуальных указателей, которые управляют блокировками: вы могли бы потребовать принудительный вызов метода drop который снимает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Rust не позволяет вызвать метод типажа
Drop вручную; вместо этого вы должны вызвать функцию std::mem::drop предоставляемую стандартной библиотекой, если хотите принудительно удалить значение до конца области видимости.
Если попытаться вызвать метод drop типажа
Drop вручную, изменяя функцию main листинга 15-14 так, как показано в листинге 15-15, мы получим ошибку компилятора:
Файл: src/main.rs
Листинг 15-15: Попытка вызвать метод
drop
из трейта
Drop
вручную для досрочной очистки
Когда мы попытаемся скомпилировать этот код, мы получим ошибку:
fn main
() { let c = CustomSmartPointer { data:
String
::from(
"some data"
),
}; println!
(
"CustomSmartPointer created."
); c.
drop
(); println!
(
"CustomSmartPointer dropped before the end of main."
);
}
$
cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example) error[E0040]: explicit use of destructor method
-->
src/main.rs:16:7
|
16 | c.drop();
| --^^^^--
| | |
| | explicit destructor calls not allowed
| help: consider using `drop` function: `drop(c)`
For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` due to previous error
а не печатать сообщение.
Раннее удаление значения с помощью std::mem::drop
К сожалению, отключение функции автоматического удаления с помощью drop является не простым. Отключение drop обычно не требуется; весь смысл типажа
Drop том, чтобы о функции позаботились автоматически. Иногда, однако, вы можете захотеть очистить значение рано. Одним из примеров является использование интеллектуальных указателей, которые управляют блокировками: вы могли бы потребовать принудительный вызов метода drop который снимает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Rust не позволяет вызвать метод типажа
Drop вручную; вместо этого вы должны вызвать функцию std::mem::drop предоставляемую стандартной библиотекой, если хотите принудительно удалить значение до конца области видимости.
Если попытаться вызвать метод drop типажа
Drop вручную, изменяя функцию main листинга 15-14 так, как показано в листинге 15-15, мы получим ошибку компилятора:
Файл: src/main.rs
Листинг 15-15: Попытка вызвать метод
drop
из трейта
Drop
вручную для досрочной очистки
Когда мы попытаемся скомпилировать этот код, мы получим ошибку:
fn main
() { let c = CustomSmartPointer { data:
String
::from(
"some data"
),
}; println!
(
"CustomSmartPointer created."
); c.
drop
(); println!
(
"CustomSmartPointer dropped before the end of main."
);
}
$
cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example) error[E0040]: explicit use of destructor method
-->
src/main.rs:16:7
|
16 | c.drop();
| --^^^^--
| | |
| | explicit destructor calls not allowed
| help: consider using `drop` function: `drop(c)`
For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` due to previous error
Это сообщение об ошибке говорит, что мы не можем явно вызывать drop
. В сообщении об ошибке используется термин деструктор (destructor), который является общим термином программирования для функции, которая очищает экземпляр. Деструктор
аналогичен конструктору, который создаёт экземпляр. Функция drop в Rust является определённым деструктором.
Rust не позволяет обращаться к drop напрямую, потому что он все равно автоматически вызовет drop в конце main
. Это вызвало бы ошибку double free, потому что в этом случае
Rust попытался бы дважды очистить одно и то же значение.
Невозможно отключить автоматическую подстановку вызова drop
, когда значение выходит из области видимости, и нельзя вызвать метод drop напрямую. Поэтому, если нам нужно принудительно избавиться от значения раньше времени, следует использовать функцию std::mem::drop
Функция std::mem::drop отличается от метода drop трейта
Drop
. Мы вызываем её,
передавая в качестве аргумента значение, которое хотим принудительно уничтожить.
Функция находится в прелюдии, поэтому мы можем изменить main в листинге 15-15 так,
чтобы вызвать функцию drop
, как показано в листинге 15-16:
Файл: src/main.rs
Листинг 15-16: Вызов
std::mem::drop
для принудительного удаления значения до того, как оно выйдет из
области видимости
Выполнение данного кода выведет следующий результат::
Текст
Dropping CustomSmartPointer with data some data
!
, напечатанный между
CustomSmartPointer created.
и текстом
CustomSmartPointer dropped before the end of main.
, показывает, что код метода drop вызывается для удаления c
в этой точке.
fn main
() { let c = CustomSmartPointer { data:
String
::from(
"some data"
),
}; println!
(
"CustomSmartPointer created."
); drop
(c); println!
(
"CustomSmartPointer dropped before the end of main."
);
}
$
cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
Вы можете использовать код, указанный в реализации типажа
Drop
, чтобы сделать очистку удобной и безопасной: например, вы можете использовать её для создания своего собственного менеджера памяти! С помощью типажа
Drop и системы владения
Rust не нужно специально заботиться о том, чтобы освобождать ресурсы, потому что
Rust делает это автоматически.
Также не нужно беспокоиться о проблемах, возникающих в результате случайной очистки значений, которые всё ещё используются: система владения, которая гарантирует, что ссылки всегда действительны, также гарантирует, что drop вызывается только один раз, когда значение больше не используется.
После того, как мы познакомились с
Box
и характеристиками умных указателей,
познакомимся с её другими умными указателями, определёнными в стандартной библиотеке.
Rc
, умный указатель с подсчётом ссылок
В большинстве ситуаций владение является однозначным: вы точно знаете, какая переменная владеет данным значением. Однако бывают случаи, когда у одного значения может быть несколько владельцев. Например, в Графовых структурах может быть несколько рёбер, указывающих на один и тот же узел — таким образом, этот узел становится фактически собственностью всех этих рёбер. Узел не подлежит удалению, за исключением тех случаев, когда на него не указывает ни одно ребро и, соответственно, у него нет владельцев.
Вы должны включить множественное владение явно, используя тип Rust
Rc
, который является аббревиатурой для подсчёта ссылок. Тип
Rc
отслеживает количество ссылок на значение, чтобы определить, используется ли оно ещё. Если ссылок на значение нет,
значение может быть очищено и при этом ни одна ссылка не станет недействительной.
Представьте себе
Rc
как телевизор в гостиной. Когда один человек входит, чтобы смотреть телевизор, он включает его. Другие могут войти в комнату и посмотреть телевизор. Когда последний человек покидает комнату, он выключает телевизор, потому что он больше не используется. Если кто-то выключит телевизор во время его просмотра другими, то оставшиеся телезрители устроят шум!
Тип
Rc
используется, когда мы хотим разместить в куче некоторые данные для чтения несколькими частями нашей программы и не можем определить во время компиляции, какая из частей завершит использование данных последней. Если бы мы знали, какая часть завершит использование последней то, мы могли бы сделать эту часть владельцем данных и вступили бы в силу обычные правила владения, применяемые во время компиляции.
Обратите внимание, что
Rc
используется только в одно поточных сценариях. Когда мы обсудим конкурентность в главе 16, мы рассмотрим, как выполнять подсчёт ссылок во много поточных программах.
Использование Rc
Давайте вернёмся к нашему примеру с cons списком в листинге 15-5. Напомним, что мы определили его с помощью типа
Box
. В этот раз мы создадим два списка, оба из которых будут владеть третьим списком. Концептуально это похоже на рисунок 15-3:
b
3 5 a
10
Nil c
4
Рисунок 15-3: Два списка,
b
и
c
, делят владение над третьим списком,
a
Мы создадим список a
, содержащий 5 и затем 10. Затем мы создадим ещё два списка: b
начинающийся с 3 и c
начинающийся с 4. Оба списка b
и c
затем продолжать первый список a
, содержащий 5 и 10. Другими словами, оба списка будут разделять первый список, содержащий 5 и 10.
Попытка реализовать этот сценарий, используя определение
List с типом
Box
не будет работать, как показано в листинге 15-17:
Файл: src/main.rs
Листинг 15-17: Демонстрация того, что нельзя иметь два списка, использующих
Box
, которые пытаются
совместно владеть третьим списком
При компиляции этого кода, мы получаем эту ошибку:
enum
List
{
Cons(
i32
,
Box
3 5 a
10
Nil c
4
Рисунок 15-3: Два списка,
b
и
c
, делят владение над третьим списком,
a
Мы создадим список a
, содержащий 5 и затем 10. Затем мы создадим ещё два списка: b
начинающийся с 3 и c
начинающийся с 4. Оба списка b
и c
затем продолжать первый список a
, содержащий 5 и 10. Другими словами, оба списка будут разделять первый список, содержащий 5 и 10.
Попытка реализовать этот сценарий, используя определение
List с типом
Box
не будет работать, как показано в листинге 15-17:
Файл: src/main.rs
Листинг 15-17: Демонстрация того, что нельзя иметь два списка, использующих
Box
, которые пытаются
совместно владеть третьим списком
При компиляции этого кода, мы получаем эту ошибку:
enum
List
{
Cons(
i32
,
Box
- ),
Nil,
} use crate::List::{Cons, Nil}; fn main
() { let a = Cons(
5
,
Box
::new(Cons(
10
,
Box
::new(Nil)))); let b = Cons(
3
,
Box
::new(a)); let c = Cons(
4
,
Box
::new(a));
}
Варианты
Cons владеют данными, которые они содержат, поэтому, когда мы создаём список b
, то a
перемещается в b
, а b
становится владельцем a
. Затем, мы пытаемся использовать a
снова при создании c
, но нам не разрешают, потому что a
был перемещён.
Мы могли бы изменить определение
Cons
, чтобы вместо этого хранить ссылки, но тогда нам пришлось бы указывать параметры времени жизни. Указывая параметры времени жизни, мы бы указали, что каждый элемент в списке будет жить как минимум столько же,
сколько и весь список. Это относится к элементам и спискам в листинге 15.17, но не во всех сценариях.
Вместо этого мы изменим наше определение типа
List так, чтобы использовать
Rc
вместо
Box
, как показано в листинге 15-18. Каждый вариант
Cons теперь будет содержать значение и тип
Rc
, указывающий на
List
. Когда мы создадим b
то,
вместо того чтобы стал владельцем a
, мы будем клонировать
Rc
который содержит a
, тем самым увеличивая количество ссылок с единицы до двойки и позволяя переменным a
и b
разделять владение на данные в типе
Rc
. Мы также клонируем a
при создании c
, увеличивая количество ссылок с двух до трёх. Каждый раз,
когда мы вызываем
Rc::clone
, счётчик ссылок на данные внутри
Rc
будет увеличиваться и данные не будут очищены, если на них нет нулевых ссылок.
Файл: src/main.rs
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: `a`
-->
src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error
1 ... 36 37 38 39 40 41 42 43 ... 62
Листинг 15-18: Определение
List
, использующее
Rc
Нам нужно добавить оператор use
, чтобы подключить тип
Rc
в область видимости,
потому что он не входит в список автоматического импорта прелюдии. В main
, мы создаём список владеющий 5 и 10, сохраняем его в новом
Rc
переменной a
Затем при создании b
и c
, мы называем функцию
Rc::clone и передаём ей ссылку на
Rc
как аргумент a
Мы могли бы вызвать a.clone()
, а не
Rc::clone(&a)
, но в Rust принято использовать
Rc::clone в таком случае. Внутренняя реализация
Rc::clone не делает глубокого копирования всех данных, как это происходит в типах большинства реализаций clone
Вызов
Rc::clone только увеличивает счётчик ссылок, что не занимает много времени.
Глубокое копирование данных может занимать много времени. Используя
Rc::clone для подсчёта ссылок, можно визуально различать виды клонирования с глубоким копированием и клонирования, которые увеличивают количество ссылок. При поиске в коде проблем с производительностью нужно рассмотреть только клонирование с глубоким копированием и игнорировать вызовы
Rc::clone
Клонирование Rc
Давайте изменим рабочий пример в листинге 15-18, чтобы увидеть как изменяется число ссылок при создании и удалении ссылок на
Rc
внутри переменной a
В листинге 15-19 мы изменим main так, чтобы она имела внутреннюю область видимости вокруг списка c
; тогда мы сможем увидеть, как меняется счётчик ссылок при выходе c
из внутренней области видимости.
Файл: src/main.rs enum
List
{
Cons(
i32
, Rc
- ),
Nil,
} use crate::List::{Cons, Nil}; use std::rc::Rc; fn main
() { let a = Rc::new(Cons(
5
, Rc::new(Cons(
10
, Rc::new(Nil))))); let b = Cons(
3
, Rc::clone(&a)); let c = Cons(
4
, Rc::clone(&a));
}
Листинг 15-19: Печать количества ссылок
В каждой части программы, где количество ссылок меняется, мы выводим количество ссылок, которое получаем, вызывая функцию
Rc::strong_count
. Эта функция названа strong_count
, а не count
, потому что тип
Rc
также имеет weak_count
; мы увидим,
для чего используется weak_count в разделе "Предотвращение циклических ссылок:
Превращение
Rc
в
Weak
"
Код выводит в консоль:
Можно увидеть, что
Rc
в переменной a
имеет начальный счётчик ссылок равный
1; затем каждый раз при вызове clone счётчик увеличивается на 1. Когда c
выходит из области видимости, счётчик уменьшается на 1. Нам не нужно вызывать функцию уменьшения счётчика ссылок, как при вызове
Rc::clone для увеличения счётчика ссылок: реализация
Drop автоматически уменьшает счётчик ссылок, когда значение
Rc
выходит из области видимости.
В этом примере мы не наблюдаем того, что когда b
, а затем a
выходят из области видимости в конце main
, счётчик становится равным 0, и
Rc
полностью очищается. Использование
Rc
позволяет одному значению иметь несколько владельцев, а счётчик гарантирует, что значение остаётся действительным до тех пор,
пока любой из владельцев ещё существует.
С помощью неизменяемых ссылок, тип
Rc
позволяет обмениваться данными между несколькими частями вашей программы только для чтения данных. Если тип
Rc
позволял бы иметь несколько изменяемых ссылок, вы могли бы нарушить одно из правил заимствования, описанных в главе 4: множественные изменяемые заимствования в одном и том же месте могут вызвать гонки данных (data races) и fn main
() { let a = Rc::new(Cons(
5
, Rc::new(Cons(
10
, Rc::new(Nil))))); println!
(
"count after creating a = {}"
, Rc::strong_count(&a)); let b = Cons(
3
, Rc::clone(&a)); println!
(
"count after creating b = {}"
, Rc::strong_count(&a));
{ let c = Cons(
4
, Rc::clone(&a)); println!
(
"count after creating c = {}"
, Rc::strong_count(&a));
} println!
(
"count after c goes out of scope = {}"
, Rc::strong_count(&a));
}
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list` count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2
несогласованность данных. Но возможность изменять данные очень полезна! В
следующем разделе мы обсудим шаблон внутренней изменчивости и тип
RefCell
,
который можно использовать вместе с
Rc
для работы с этим ограничением.
следующем разделе мы обсудим шаблон внутренней изменчивости и тип
RefCell
,
который можно использовать вместе с
Rc
для работы с этим ограничением.
RefCell
и шаблон внутренней изменяемости
Внутренняя изменяемость - это паттерн проектирования Rust, который позволяет вам изменять данные даже при наличии неизменяемых ссылок на эти данные; обычно такое действие запрещено правилами заимствования. Для изменения данных паттерн использует unsafe код внутри структуры данных, чтобы обойти обычные правила Rust,
регулирующие изменяемость и заимствование. Небезопасный (unsafe) код даёт понять компилятору, что мы самостоятельно следим за соблюдением этих правил, а не полагаемся на то, что компилятор будет делать это для нас; подробнее о небезопасном коде мы поговорим в главе 19.
Мы можем использовать типы, в которых применяется паттерн внутренней изменяемости, только если мы можем гарантировать, что правила заимствования будут соблюдаться во время выполнения, несмотря на то, что компилятор не сможет этого гарантировать. В этом случае небезопасный код оборачивается безопасным API, и внешне тип остаётся неизменяемым.
Давайте изучим данную концепцию с помощью типа данных
RefCell
, который реализует этот шаблон.
Применение правил заимствования во время выполнения с помощью
RefCell
В отличие от
Rc
тип
RefCell
предоставляет единоличное владение данными,
которые он содержит. В чем же отличие типа
RefCell
от
Box
? Давайте вспомним правила заимствования из Главы 4:
В любой момент времени вы можете иметь либо одну изменяемую ссылку либо сколько угодно неизменяемых ссылок (но не оба типа ссылок одновременно).
Ссылки всегда должны быть действительными.
С помощью ссылок и типа
Box
инварианты правил заимствования применяются на этапе компиляции. С помощью
RefCell
они применяются во время работы
программы. Если вы нарушите эти правила, работая с ссылками, то будет ошибка компиляции. Если вы работаете с
RefCell
и нарушите эти правила, то программа вызовет панику и завершится.
Преимущества проверки правил заимствования во время компиляции состоят в том, что ошибки будут обнаруживаться быстрее в процессе разработки и это не влияет на производительность во время выполнения программы, поскольку весь анализ выполняется заранее. По этим причинам проверка правил заимствования во время компиляции является лучшим выбором в большинстве случаев, именно поэтому она используется в Rust по умолчанию.
Преимущество проверки правил заимствования во время выполнения заключается в том, что определённые сценарии, безопасные для памяти, разрешаются там, где они были бы запрещены проверкой во время компиляции. Статический анализ, как и компилятор Rust, по своей сути консервативен. Некоторые свойства кода невозможно обнаружить, анализируя код: самый известный пример - проблема остановки, которая выходит за рамки этой книги, но является интересной темой для исследования.
Поскольку некоторый анализ невозможен, то если компилятор Rust не может быть уверен, что код соответствует правилам владения, он может отклонить корректную программу; таким образом он является консервативным. Если Rust принял некорректную программу, то пользователи не смогут доверять гарантиям, которые даёт Rust. Однако,
если Rust отклонит корректную программу, то программист будет испытывать неудобства, но ничего катастрофического не произойдёт. Тип
RefCell
полезен, когда вы уверены, что ваш код соответствует правилам заимствования, но компилятор не может понять и гарантировать этого.
Подобно типу
Rc
, тип
RefCell
предназначен только для использования в одно поточных сценариях и выдаст ошибку времени компиляции, если вы попытаетесь использовать его в много поточном контексте. Мы поговорим о том, как получить функциональность
RefCell
во много поточной программе в главе 16.
Вот список причин выбора типов
Box
,
Rc
или
RefCell
:
Тип
Rc
разрешает множественное владение одними и теми же данными; типы
Box
и
RefCell
разрешают иметь единственных владельцев.
Тип
Box
разрешает неизменяемые или изменяемые владения, проверенные при компиляции; тип
Rc
разрешает только неизменяемые владения, проверенные при компиляции; тип
RefCell
разрешает неизменяемые или изменяемые владения, проверенные во время выполнения.
Поскольку
RefCell
разрешает изменяемые заимствования, проверенные во время выполнения, можно изменять значение внутри
RefCell
даже если
RefCell
является неизменным.
Изменение значения внутри неизменного значения является шаблоном внутренней
изменяемости (interior mutability). Давайте посмотрим на ситуацию, в которой внутренняя изменяемость полезна и рассмотрим, как это возможно.
Внутренняя изменяемость: изменяемое заимствование
неизменяемого значения
Следствием правил заимствования является то, что когда у вас есть неизменяемое значение, вы не можете заимствовать его с изменением. Например, этот код не будет компилироваться:
Если вы попытаетесь скомпилировать этот код, вы получите следующую ошибку:
Однако бывают ситуации, в которых было бы полезно, чтобы объект мог изменять себя при помощи своих методов, но казался неизменным для прочего кода. Код вне методов этого объекта не должен иметь возможности изменять его содержимое. Использование
RefCell
- один из способов получить возможность внутренней изменяемости, но при этом
RefCell
не позволяет полностью обойти правила заимствования: средство проверки правил заимствования в компиляторе позволяет эту внутреннюю изменяемость, однако правила заимствования проверяются во время выполнения. Если вы нарушите правила, то вместо ошибки компиляции вы получите panic!
Давайте разберём практический пример, в котором мы можем использовать
RefCell
для изменения неизменяемого значения и посмотрим, почему это полезно.
Вариант использования внутренней изменяемости: мок объекты
Иногда во время тестирования программист использует один тип вместо другого для того, чтобы проверить определённое поведение и убедиться, что оно реализовано правильно. Такой тип-заместитель называется тестовым дублёром. Воспринимайте его как "каскадёра" в кинематографе, когда дублёр заменяет актёра для выполнения определённой сложной сцены. Тестовые дублёры заменяют другие типы при выполнении тестов. {Инсценировочные (Mock) объекты - это особый тип тестовых дублёров, которые сохраняют данные происходящих во время теста действий тем самым позволяя вам убедиться впоследствии, что все действия были выполнены правильно.
В Rust нет объектов в том же смысле, в каком они есть в других языках и в Rust нет функциональности мок объектов, встроенных в стандартную библиотеку, как в некоторых других языках. Однако вы определённо можете создать структуру, которая будет служить тем же целям, что и мок объект.
fn main
() { let x =
5
; let y = &
mut x;
}
$
cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing) error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
-->
src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`. error: could not compile `borrowing` due to previous error
Вот сценарий, который мы будем тестировать: мы создадим библиотеку, которая отслеживает значение по отношению к заранее определённому максимальному значению и отправляет сообщения в зависимости от того, насколько текущее значение находится близко к такому максимальному значению. Эта библиотека может использоваться, например, для отслеживания квоты количества вызовов API
пользователя, которые ему разрешено делать.
Наша библиотека будет предоставлять только функции отслеживания того, насколько близко к максимальному значению находится значение и какие сообщения должны быть внутри в этот момент. Ожидается, что приложения, использующие нашу библиотеку,
предоставят механизм для отправки сообщений: приложение может поместить сообщение в приложение, отправить электронное письмо, отправить текстовое сообщение или что-то ещё. Библиотеке не нужно знать эту деталь. Все что ему нужно - это что-то, что реализует типаж, который мы предоставим с названием
Messenger
Листинг 15-20 показывает код библиотеки:
Файл: src/lib.rs
Листинг 15-20: Библиотека для отслеживания степени приближения того или иного значения к
максимально допустимой величине и предупреждения, в случае если значение достигает определённого
уровня
Одна важная часть этого кода состоит в том, что типаж
Messenger имеет один метод send
, принимающий аргументами неизменяемую ссылку на self и текст сообщения. Он является интерфейсом, который должен иметь наш мок объект. Другой важной частью является то, что мы хотим проверить поведение метода set_value у типа
LimitTracker
Мы можем изменить значение, которое передаём параметром value
, но set_value ничего не возвращает и нет основания, чтобы мы могли бы проверить утверждения о выполнении метода. Мы хотим иметь возможность сказать, что если мы создаём
LimitTracker с чем-то, что реализует типаж
Messenger и с определённым значением для max
, то когда мы передаём разные числа в переменной value экземпляр self.messenger отправляет соответствующие сообщения.
pub trait
Messenger
{ fn send
(&
self
, msg: &
str
);
} pub struct
LimitTracker
<
'a
, T: Messenger> { messenger: &
'a
T, value: usize
, max: usize
,
} impl
<
'a
, T> LimitTracker<
'a
, T> where
T: Messenger,
{ pub fn new
(messenger: &
'a
T, max: usize
) -> LimitTracker<
'a
, T> {
LimitTracker { messenger, value:
0
, max,
}
} pub fn set_value
(&
mut self
, value: usize
) { self
.value = value; let percentage_of_max = self
.value as f64
/ self
.max as f64
; if percentage_of_max >=
1.0
{ self
.messenger.send(
"Error: You are over your quota!"
);
} else if percentage_of_max >=
0.9
{ self
.messenger
.send(
"Urgent warning: You've used up over 90% of your quota!"
);
} else if percentage_of_max >=
0.75
{ self
.messenger
.send(
"Warning: You've used up over 75% of your quota!"
);
}
}
}
Нам нужен мок объект, который вместо отправки электронного письма или текстового сообщения будет отслеживать сообщения, которые были ему поручены для отправки через send
. Мы можем создать новый экземпляр мок объекта, создать
LimitTracker с
использованием мок объект для него, вызвать метод set_value у экземпляра
LimitTracker
, а затем проверить, что мок объект имеет ожидаемое сообщение. В
листинге 15-21 показана попытка реализовать мок объект, чтобы сделать именно то что хотим, но анализатор заимствований не разрешит такой код:
Файл: src/lib.rs
Листинг 15-21: Попытка реализовать
MockMessenger
, которая не была принята механизмом проверки
заимствований
Этот тестовый код определяет структуру
MockMessenger
, в которой есть поле sent_messages со значениями типа
Vec из
String для отслеживания сообщений,
которые поручены структуре для отправки. Мы также определяем ассоциированную функцию new
, чтобы было удобно создавать новые экземпляры
MockMessenger
, которые создаются с пустым списком сообщений. Затем мы реализуем типаж
Messenger для типа
MockMessenger
, чтобы передать
MockMessenger в
LimitTracker
. В сигнатуре метода send
#[cfg(test)]
mod tests { use super::*; struct
MockMessenger
{ sent_messages:
Vec
<
String
>,
} impl
MockMessenger { fn new
() -> MockMessenger {
MockMessenger { sent_messages: vec!
[],
}
}
} impl
Messenger for
MockMessenger { fn send
(&
self
, message: &
str
) { self
.sent_messages.push(
String
::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message
() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger,
100
); limit_tracker.set_value(
80
); assert_eq!
(mock_messenger.sent_messages.len(),
1
);
}
}
мы принимаем сообщение для передачи в качестве параметра и сохраняем его в
MockMessenger внутри списка sent_messages
В этом тесте мы проверяем, что происходит, когда
LimitTracker сказано установить value в значение, превышающее 75 процентов от значения max
. Сначала мы создаём новый
MockMessenger
, который будет иметь пустой список сообщений. Затем мы создаём новый
LimitTracker и передаём ему ссылку на новый
MockMessenger и max значение равное 100. Мы вызываем метод set_value у
LimitTracker со значением 80, что составляет более 75 процентов от 100. Затем мы с помощью утверждения проверяем, что
MockMessenger должен содержать одно сообщение из списка внутренних сообщений.
Однако с этим тестом есть одна проблема, показанная ниже:
Мы не можем изменять
MockMessenger для отслеживания сообщений, потому что метод send принимает неизменяемую ссылку на self
. Мы также не можем принять предложение из текста ошибки, чтобы использовать
&mut self
, потому что тогда сигнатура send не будет соответствовать сигнатуре в определении типажа
Messenger
(не стесняйтесь попробовать и посмотреть, какое сообщение об ошибке получите вы).
Это ситуация, в которой внутренняя изменяемость может помочь! Мы сохраним sent_messages внутри типа
RefCell
, а затем в методе send сообщение сможет изменить список sent_messages для хранения сообщений, которые мы видели. Листинг
15-22 показывает, как это выглядит:
Файл: src/lib.rs
$
cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
-->
src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference:
`&mut self`
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` due to previous error warning: build failed, waiting for other jobs to finish...
MockMessenger внутри списка sent_messages
В этом тесте мы проверяем, что происходит, когда
LimitTracker сказано установить value в значение, превышающее 75 процентов от значения max
. Сначала мы создаём новый
MockMessenger
, который будет иметь пустой список сообщений. Затем мы создаём новый
LimitTracker и передаём ему ссылку на новый
MockMessenger и max значение равное 100. Мы вызываем метод set_value у
LimitTracker со значением 80, что составляет более 75 процентов от 100. Затем мы с помощью утверждения проверяем, что
MockMessenger должен содержать одно сообщение из списка внутренних сообщений.
Однако с этим тестом есть одна проблема, показанная ниже:
Мы не можем изменять
MockMessenger для отслеживания сообщений, потому что метод send принимает неизменяемую ссылку на self
. Мы также не можем принять предложение из текста ошибки, чтобы использовать
&mut self
, потому что тогда сигнатура send не будет соответствовать сигнатуре в определении типажа
Messenger
(не стесняйтесь попробовать и посмотреть, какое сообщение об ошибке получите вы).
Это ситуация, в которой внутренняя изменяемость может помочь! Мы сохраним sent_messages внутри типа
RefCell
, а затем в методе send сообщение сможет изменить список sent_messages для хранения сообщений, которые мы видели. Листинг
15-22 показывает, как это выглядит:
Файл: src/lib.rs
$
cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
-->
src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference:
`&mut self`
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` due to previous error warning: build failed, waiting for other jobs to finish...
1 ... 37 38 39 40 41 42 43 44 ... 62
Листинг 15-22: Использование
RefCell
для изменения внутреннего значения, в то время как внешнее
значение считается неизменяемым
Поле sent_messages теперь имеет тип
RefCell
вместо
Vec
. В
функции new мы создаём новый экземпляр
RefCell
для пустого вектора.
Для реализации метода send первый параметр по-прежнему является неизменяемым для заимствования self
, которое соответствует определению типажа. Мы вызываем borrow_mut для
RefCell
в self.sent_messages
, чтобы получить изменяемую ссылку на значение внутри
RefCell
, которое является вектором. Затем мы можем вызвать push у изменяемой ссылки на вектор, чтобы отслеживать сообщения, отправленные во время теста.
Последнее изменение, которое мы должны сделать, заключается в утверждении для проверки: чтобы увидеть, сколько элементов находится во внутреннем векторе, мы вызываем метод borrow у
RefCell
, чтобы получить неизменяемую ссылку на внутренний вектор сообщений.
Теперь, когда вы увидели как использовать
RefCell
, давайте изучим как он работает!
Отслеживание заимствований во время выполнения с помощью RefCell
#[cfg(test)]
mod tests { use super::*; use std::cell::RefCell; struct
MockMessenger
{ sent_messages: RefCell<
Vec
<
String
>>,
} impl
MockMessenger { fn new
() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(
vec!
[]),
}
}
} impl
Messenger for
MockMessenger { fn send
(&
self
, message: &
str
) { self
.sent_messages.borrow_mut().push(
String
::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message
() {
// --snip-- assert_eq!
(mock_messenger.sent_messages.borrow().len(),
1
);
}
}
При создании неизменных и изменяемых ссылок мы используем синтаксис
&
и
&mut соответственно. У типа
RefCell
, мы используем методы borrow и borrow_mut
,
которые являются частью безопасного API, который принадлежит
RefCell
. Метод borrow возвращает тип умного указателя
Ref
, метод borrow_mut возвращает тип умного указателя
RefMut
. Оба типа реализуют типаж
Deref
, поэтому мы можем рассматривать их как обычные ссылки.
Тип
RefCell
отслеживает сколько умных указателей
Ref
и
RefMut
активны в данное время. Каждый раз, когда мы вызываем borrow
, тип
RefCell
увеличивает количество активных заимствований. Когда значение
Ref
выходит из области видимости, то количество неизменяемых заимствований уменьшается на единицу. Как и с правилами заимствования во время компиляции,
RefCell
позволяет иметь много неизменяемых заимствований или одно изменяемое заимствование в любой момент времени.
Если попытаться нарушить эти правила, то вместо получения ошибки компилятора, как это было бы со ссылками, реализация
RefCell
будет вызывать панику во время выполнения. В листинге 15-23 показана модификация реализации send из листинга 15-
22. Мы намеренно пытаемся создать два изменяемых заимствования активных для одной и той же области видимости, чтобы показать как
RefCell
не позволяет нам делать так во время выполнения.
Файл: src/lib.rs
Листинг 15-23: Создание двух изменяемых ссылок в одной области видимости, чтобы убедиться, что
RefCell
вызовет панику
Мы создаём переменную one_borrow для умного указателя
RefMut
возвращаемого из метода borrow_mut
. Затем мы создаём другое изменяемое заимствование таким же образом в переменной two_borrow
. Это создаёт две изменяемые ссылки в одной области видимости, что недопустимо. Когда мы запускаем тесты для нашей библиотеки, код в листинге 15-23 компилируется без ошибок, но тест завершится неудачно:
impl
Messenger for
MockMessenger { fn send
(&
self
, message: &
str
) { let mut one_borrow = self
.sent_messages.borrow_mut(); let mut two_borrow = self
.sent_messages.borrow_mut(); one_borrow.push(
String
::from(message)); two_borrow.push(
String
::from(message));
}
}
Обратите внимание, что код вызвал панику с сообщением already borrowed:
BorrowMutError
. Вот так тип
RefCell
обрабатывает нарушения правил заимствования во время выполнения.
Решение отлавливать ошибки заимствования во время выполнения, а не во время компиляции, как мы сделали здесь, означает, что вы потенциально будете находить ошибки в своём коде на более поздних этапах разработки: возможно, не раньше, чем ваш код будет развернут в рабочем окружении. Кроме того, ваш код будет иметь небольшие потери производительности в процессе работы, поскольку заимствования будут отслеживаться во время выполнения, а не во время компиляции. Однако использование
RefCell
позволяет написать объект-имитатор, который способен изменять себя, чтобы сохранять сведения о тех значениях, которые он получал, пока вы использовали его в контексте, где разрешены только неизменяемые значения. Вы можете использовать
RefCell
, несмотря на его недостатки, чтобы получить больше функциональности, чем дают обычные ссылки.
Наличие нескольких владельцев изменяемых данных путём
объединения типов Rc
Обычный способ использования
RefCell
заключается в его сочетании с типом
Rc
. Напомним, что тип
Rc
позволяет иметь нескольких владельцев некоторых данных, но даёт только неизменяемый доступ к этим данным. Если у вас есть
Rc
,
который внутри содержит тип
RefCell
, вы можете получить значение, которое может иметь несколько владельцев и которое можно изменять!
$
cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker- e599811fa246dbde) running 1 test test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
Например, вспомните пример cons списка листинга 15-18, где мы использовали
Rc
,
чтобы несколько списков могли совместно владеть другим списком. Поскольку
Rc
содержит только неизменяемые значения, мы не можем изменить ни одно из значений в списке после того, как мы их создали. Давайте добавим тип
RefCell
, чтобы получить возможность изменять значения в списках. В листинге 15-24 показано использование
RefCell
в определении
Cons так, что мы можем изменить значение хранящееся во всех списках:
Файл: src/main.rs
Листинг 15-24: Использование
Rc
для создания
List
, который мы можем изменять
Мы создаём значение, которое является экземпляром
Rc
и сохраняем его в переменной с именем value
, чтобы получить к ней прямой доступ позже. Затем мы создаём
List в переменной a
с вариантом
Cons
, который содержит value
. Нам нужно вызвать клонирование value
, так как обе переменные a
и value владеют внутренним значением
5
, а не передают владение из value в переменную a
или не выполняют заимствование с помощью a
переменной value
Мы оборачиваем список у переменной a
в тип
Rc
, поэтому при создании списков в переменные b
и c
они оба могут ссылаться на a
, что мы и сделали в листинге 15-18.
После создания списков a
, b
и c
мы хотим добавить 10 к значению в value
. Для этого вызовем borrow_mut у value
, который использует функцию автоматического разыменования, о которой мы говорили в главе 5 (см. раздел "Где находится оператор
-
>
?"
) во внутреннее значение
RefCell
. Метод borrow_mut возвращает умный
#[derive(Debug)]
enum
List
{
Cons(Rc
>>, Rc
- ),
Nil,
} use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main
() { let value = Rc::new(RefCell::new(
5
)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(
3
)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(
4
)), Rc::clone(&a));
*value.borrow_mut() +=
10
; println!
(
"a after = {:?}"
, a); println!
(
"b after = {:?}"
, b); println!
(
"c after = {:?}"
, c);
}
указатель
RefMut
, и мы используя оператор разыменования, изменяем внутреннее значение.
Когда мы печатаем a
, b
и c
то видим, что все они имеют изменённое значение равное
15, а не 5:
Эта техника довольно изящна! Используя
RefCell
, мы получаем внешне неизменяемое значение
List
. Но мы можем использовать методы
RefCell
, которые предоставляют доступ к его внутренностям, чтобы мы могли изменять наши данные,
когда это необходимо. Проверка правил заимствования во время выполнения защищает нас от гонок данных, и иногда стоит немного пожертвовать производительностью ради такой гибкости наших структур данных. Обратите внимание, что
RefCell
не работает для многопоточного кода!
Mutex
- это thread-safe версия
RefCell
, а
Mutex
мы обсудим в главе 16.
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list` a after = Cons(RefCell { value: 15 }, Nil) b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
RefMut
, и мы используя оператор разыменования, изменяем внутреннее значение.
Когда мы печатаем a
, b
и c
то видим, что все они имеют изменённое значение равное
15, а не 5:
Эта техника довольно изящна! Используя
RefCell
, мы получаем внешне неизменяемое значение
List
. Но мы можем использовать методы
RefCell
, которые предоставляют доступ к его внутренностям, чтобы мы могли изменять наши данные,
когда это необходимо. Проверка правил заимствования во время выполнения защищает нас от гонок данных, и иногда стоит немного пожертвовать производительностью ради такой гибкости наших структур данных. Обратите внимание, что
RefCell
не работает для многопоточного кода!
Mutex
- это thread-safe версия
RefCell
, а
Mutex
мы обсудим в главе 16.
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list` a after = Cons(RefCell { value: 15 }, Nil) b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Ссылочные зацикливания могут приводить к утечке
памяти
Гарантии безопасности памяти в Rust затрудняют, но не делают невозможным случайное выделение памяти, которое никогда не очищается (известное как утечка памяти ).
Полное предотвращение утечек памяти не является одной из гарантий Rust, а это означает, что утечки памяти безопасны в Rust. Мы видим, что Rust допускает утечку памяти с помощью
Rc
и
RefCell
: можно создавать ссылки, в которых элементы ссылаются друг на друга в цикле. Это создаёт утечки памяти, потому что счётчик ссылок каждого элемента в цикле никогда не достигнет 0, а значения никогда не будут удалены.
Создание ссылочного зацикливания
Давайте посмотрим, как может произойти ситуация ссылочного зацикливания и как её
предотвратить, начиная с определения перечисления
List и метода tail в листинге
15-25:
Файл : src/main.rs
Листинг 15-25: Объявление cons list, который содержит
RefCell
, чтобы мы могли изменять то, на что
ссылается экземпляр
Cons
Мы используем другую вариацию определения
List из листинга 15-5. Второй элемент в варианте
Cons теперь
RefCell
, что означает, что вместо возможности менять значение i32
, как мы делали в листинге 15-24, мы хотим менять значение
List
, на которое указывает вариант
Cons
. Мы также добавляем метод tail
, чтобы нам было удобно обращаться ко второму элементу, если у нас есть вариант
Cons use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc;
#[derive(Debug)]
enum
List
{
Cons(
i32
, RefCell
Nil,
} impl
List { fn tail
(&
self
) ->
Option
<&RefCell
{
Cons(_, item) =>
Some
(item),
Nil =>
None
,
}
}
} fn main
() {}
В листинге 15-26 мы добавляем main функцию, которая использует определения листинга 15-25. Этот код создаёт список в переменной a
и список b
, который указывает на список a
. Затем он изменяет список внутри a
так, чтобы он указывал на b
, создавая ссылочное зацикливание. В коде есть инструкции println!
, чтобы показать значения счётчиков ссылок в различных точках этого процесса.
Файл : src/main.rs
Листинг 15-26: Создание ссылочного цикла из двух значений
List
, указывающих друг на друга
Мы создаём экземпляр
Rc
содержащий значение
List в переменной a
с начальным списком
5, Nil
. Затем мы создаём экземпляр
Rc
содержащий другое значение
List в переменной b
, которое содержит значение 10 и указывает на список в a
Мы меняем a
так, чтобы он указывал на b
вместо
Nil
, создавая зацикленность. Мы делаем это с помощью метода tail
, чтобы получить ссылку на
RefCell
из переменной a
, которую мы помещаем в переменную link
. Затем мы используем метод borrow_mut из типа
RefCell
, чтобы изменить внутреннее значение типа
Rc
, содержащего начальное значение
Nil на значение типа
Rc
взятое из переменной b
Когда мы запускаем этот код, оставив последний println!
закомментированным в данный момент, мы получим вывод:
fn main
() { let a = Rc::new(Cons(
5
, RefCell::new(Rc::new(Nil)))); println!
(
"a initial rc count = {}"
, Rc::strong_count(&a)); println!
(
"a next item = {:?}"
, a.tail()); let b = Rc::new(Cons(
10
, RefCell::new(Rc::clone(&a)))); println!
(
"a rc count after b creation = {}"
, Rc::strong_count(&a)); println!
(
"b initial rc count = {}"
, Rc::strong_count(&b)); println!
(
"b next item = {:?}"
, b.tail()); if let
Some
(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
} println!
(
"b rc count after changing a = {}"
, Rc::strong_count(&b)); println!
(
"a rc count after changing a = {}"
, Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
Количество ссылок на экземпляры
Rc
как в a
, так и в b
равно 2 после того, как мы заменили список в a
на ссылку на b
. В конце main
Rust уничтожает переменную b
,
что уменьшает количество ссылок на
Rc
из b
с 2 до 1. Память, которую
Rc
занимает в куче, не будет освобождена в этот момент, потому что количество ссылок на неё равно 1, а не 0. Затем Rust удаляет a
, что уменьшает количество ссылок экземпляра
Rc
в a
с 2 до 1. Память этого экземпляра также не может быть освобождена,
поскольку другой экземпляр
Rc
по-прежнему ссылается на него. Таким образом,
память, выделенная для списка не будет освобождена никогда. Чтобы наглядно представить этот цикл ссылок, мы создали диаграмму на рисунке 15-4.
5 10
a b
Рисунок 15-4: Ссылочный цикл списков
a
и
b
, указывающих друг на друга
Если вы удалите последний комментарий с println!
и запустите программу, Rust будет пытаться печатать зацикленность в a
, указывающей на b
, указывающей на a
и так далее, пока не переполниться стек.
По сравнению с реальной программой, последствия создания цикла ссылок в этом примере не так страшны: сразу после создания цикла ссылок программа завершается.
$
cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list` a initial rc count = 1 a next item = Some(RefCell { value: Nil }) a rc count after b creation = 2 b initial rc count = 1 b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) }) b rc count after changing a = 2 a rc count after changing a = 2
Однако если более сложная программа выделит много памяти в цикле и будет удерживать её в течение длительного времени, программа будет потреблять больше памяти, чем ей нужно, и может перенапрячь систему, что приведёт к исчерпанию доступной памяти.
Вызвать образование ссылочной зацикленности не просто, но и не невозможно. Если у вас есть значения
RefCell
которые содержат значения
Rc
или аналогичные вложенные комбинации типов с внутренней изменчивостью и подсчётом ссылок, вы должны убедиться, что вы не создаёте зацикленность; Вы не можете полагаться на то,
что Rust их обнаружит. Создание ссылочной зацикленности являлось бы логической ошибкой в программе, для которой вы должны использовать автоматические тесты,
проверку кода и другие практики разработки программного обеспечения для её
минимизации.
Другое решение для избежания ссылочной зацикленности - это реорганизация ваших структур данных, чтобы некоторые ссылки выражали владение, а другие - отсутствие владения. В результате можно иметь циклы, построенные на некоторых отношениях владения и некоторые не основанные на отношениях владения, тогда только отношения владения влияют на то, можно ли удалить значение. В листинге 15-25 мы всегда хотим,
чтобы варианты
Cons владели своим списком, поэтому реорганизация структуры данных невозможна. Давайте рассмотрим пример с использованием графов, состоящих из родительских и дочерних узлов, чтобы увидеть, когда отношения владения не являются подходящим способом предотвращения ссылочной зацикленности.
Предотвращение ссылочной зацикленности: замена умного указателя
Rc
на Weak
До сих пор мы демонстрировали, что вызов
Rc::clone увеличивает strong_count экземпляра
Rc
, а экземпляр
Rc
удаляется, только если его strong_count равен 0.
Вы также можете создать слабую ссылку на значение внутри экземпляра
Rc
, вызвав
Rc::downgrade и передав ссылку на
Rc
. Сильные ссылки - это то с помощью чего вы можете поделиться владением экземпляра
Rc
. Слабые ссылки не отражают связи владения, и их подсчёт не влияет на то, когда экземпляр
Rc
будет очищен. Они не приведут к ссылочному циклу, потому что любой цикл, включающий несколько слабых ссылок, будет разорван, как только количество сильных ссылок для задействованных значений станет равным 0.
Когда вы вызываете
Rc::downgrade
, вы получаете умный указатель типа
Weak
. Вместо того чтобы увеличить strong_count в экземпляре
Rc
на 1, вызов
Rc::downgrade увеличивает weak_count на 1. Тип
Rc
использует weak_count для отслеживания количества существующих ссылок
Weak
, аналогично strong_count
. Разница в том,
что weak_count не должен быть равен 0, чтобы экземпляр
Rc
мог быть удалён.
Поскольку значение, на которое ссылается
Weak
могло быть удалено, то необходимо убедиться, что это значение все ещё существует, чтобы сделать что-либо со значением на которое указывает
Weak
. Делайте это вызывая метод upgrade у экземпляра типа
Weak
, который вернёт
Option
. Вы получите результат
Some
, если значение
Rc
ещё не было удалено и результат
None
, если значение
Rc
было удалено.
Поскольку upgrade возвращает тип
Option
, Rust обеспечит обработку обоих случаев
Some и
None и не будет некорректного указателя.
В качестве примера, вместо того чтобы использовать список чей элемент знает только о следующем элементе, мы создадим дерево, чьи элементы знают о своих дочерних элементах и о своих родительских элементах.
1 ... 38 39 40 41 42 43 44 45 ... 62
Создание древовидной структуры данных: Node с дочерними узлами
Для начала мы построим дерево с узлами, которые знают о своих дочерних узлах. Мы создадим структуру с именем
Node
, которая будет содержать собственное значение i32
,
а также ссылки на его дочерние значения
Node
:
Файл : src/main.rs
Мы хотим, чтобы
Node владел своими дочерними узлами и мы хотим поделиться этим владением с переменными так, чтобы мы могли напрямую обращаться к каждому
Node в
дереве. Для этого мы определяем внутренние элементы типа
Vec
как значения типа
Rc
. Мы также хотим изменять те узлы, которые являются дочерними по отношению к другому узлу, поэтому у нас есть тип
RefCell
в поле children оборачивающий тип
Vec
Далее мы будем использовать наше определение структуры и создадим один экземпляр
Node с именем leaf со значением 3 и без дочерних элементов, а другой экземпляр с именем branch со значением 5 и leaf в качестве одного из его дочерних элементов, как показано в листинге 15-27:
Файл : src/main.rs use std::cell::RefCell; use std::rc::Rc;
#[derive(Debug)]
struct
Node
{ value: i32
, children: RefCell<
Vec
}
Листинг 15-27: Создание узла
leaf
без дочерних элементов и узла
branch
с
leaf
в качестве одного из
дочерних элементов
Мы клонируем содержимое
Rc
из переменной leaf и сохраняем его в переменной branch
, что означает, что
Node в leaf теперь имеет двух владельцев: leaf и branch
. Мы можем получить доступ из branch к leaf через обращение branch.children
, но нет способа добраться из leaf к branch
. Причина в том, что leaf не имеет ссылки на branch и не знает, что они связаны. Мы хотим, чтобы leaf знал, что branch является его родителем. Мы сделаем это далее.
Добавление ссылки от ребёнка к его родителю
Для того, чтобы дочерний узел знал о своём родительском узле нужно добавить поле parent в наше определение структуры
Node
. Проблема в том, чтобы решить, каким должен быть тип parent
. Мы знаем, что он не может содержать
Rc
, потому что это создаст ссылочную зацикленность с leaf.parent указывающей на branch и branch.children
, указывающей на leaf
, что приведёт к тому, что их значения strong_count никогда не будут равны 0.
Подумаем об этих отношениях по-другому, родительский узел должен владеть своими потомками: если родительский узел удаляется, его дочерние узлы также должны быть удалены. Однако дочерний элемент не должен владеть своим родителем: если мы удаляем дочерний узел то родительский элемент все равно должен существовать. Это случай для использования слабых ссылок!
Поэтому вместо
Rc
мы сделаем так, чтобы поле parent использовало тип
Weak
, а именно
RefCell
. Теперь наше определение структуры
Node выглядит так:
Файл : src/main.rs fn main
() { let leaf = Rc::new(Node { value:
3
, children: RefCell::new(
vec!
[]),
}); let branch = Rc::new(Node { value:
5
, children: RefCell::new(
vec!
[Rc::clone(&leaf)]),
});
}
Узел сможет ссылаться на свой родительский узел, но не владеет своим родителем. В
листинге 15-28 мы обновляем main на использование нового определения так, чтобы у узла leaf был бы способ ссылаться на его родительский узел branch
:
Файл : src/main.rs
Листинг 15-28: Узел
leaf
со слабой ссылкой на его родительский узел
branch
Создание узла leaf выглядит аналогично примеру из Листинга 15-27, за исключением поля parent
: leaf изначально не имеет родителя, поэтому мы создаём новый, пустой экземпляр ссылки
Weak
На этом этапе, когда мы пытаемся получить ссылку на родительский узел у узла leaf с
помощью метода upgrade
, мы получаем значение
None
. Мы видим это в выводе первого println!
выражения:
Когда мы создаём узел branch у него также будет новая ссылка типа
Weak
в поле parent
, потому что узел branch не имеет своего родительского узла. У нас все ещё есть leaf как один из потомков узла branch
. Когда мы получили экземпляр
Node в
use std::cell::RefCell; use std::rc::{Rc, Weak};
#[derive(Debug)]
struct
Node
{ value: i32
, parent: RefCell
Vec
} fn main
() { let leaf = Rc::new(Node { value:
3
, parent: RefCell::new(Weak::new()), children: RefCell::new(
vec!
[]),
}); println!
(
"leaf parent = {:?}"
, leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value:
5
, parent: RefCell::new(Weak::new()), children: RefCell::new(
vec!
[Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!
(
"leaf parent = {:?}"
, leaf.parent.borrow().upgrade());
} leaf parent = None
переменной branch
, мы можем изменить переменную leaf чтобы дать ей
Weak
ссылку на её родителя. Мы используем метод borrow_mut у типа
RefCell>
поля parent у leaf
, а затем используем функцию
Rc::downgrade для создания
Weak
ссылки на branch из
Rc
в branch
Когда мы снова напечатаем родителя leaf то в этот раз мы получим вариант
Some содержащий branch
, теперь leaf может получить доступ к своему родителю! Когда мы печатаем leaf
, мы также избегаем цикла, который в конечном итоге заканчивался переполнением стека, как в листинге 15-26; ссылки типа
Weak
печатаются как
(Weak)
:
Отсутствие бесконечного вывода означает, что этот код не создал ссылочной зацикленности. Мы также можем сказать это, посмотрев на значения, которые мы получаем при вызове
Rc::strong_count и
Rc::weak_count
Визуализация изменений в strong_count и weak_count
Давайте посмотрим, как изменяются значения strong_count и weak_count экземпляров типа
Rc
с помощью создания новой внутренней области видимости и перемещая создания экземпляра branch в эту область. Таким образом можно увидеть, что происходит, когда branch создаётся и затем удаляется при выходе из области видимости.
Изменения показаны в листинге 15-29:
Файл : src/main.rs leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
, мы можем изменить переменную leaf чтобы дать ей
Weak
ссылку на её родителя. Мы используем метод borrow_mut у типа
RefCell
поля parent у leaf
, а затем используем функцию
Rc::downgrade для создания
Weak
ссылки на branch из
Rc
в branch
Когда мы снова напечатаем родителя leaf то в этот раз мы получим вариант
Some содержащий branch
, теперь leaf может получить доступ к своему родителю! Когда мы печатаем leaf
, мы также избегаем цикла, который в конечном итоге заканчивался переполнением стека, как в листинге 15-26; ссылки типа
Weak
печатаются как
(Weak)
:
Отсутствие бесконечного вывода означает, что этот код не создал ссылочной зацикленности. Мы также можем сказать это, посмотрев на значения, которые мы получаем при вызове
Rc::strong_count и
Rc::weak_count
Визуализация изменений в strong_count и weak_count
Давайте посмотрим, как изменяются значения strong_count и weak_count экземпляров типа
Rc
с помощью создания новой внутренней области видимости и перемещая создания экземпляра branch в эту область. Таким образом можно увидеть, что происходит, когда branch создаётся и затем удаляется при выходе из области видимости.
Изменения показаны в листинге 15-29:
Файл : src/main.rs leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
Листинг 15-29: Создание
branch
во внутренней области видимости и подсчёт сильных и слабых ссылок
После того, как leaf создан его
Rc
имеет значения strong count равное 1 и weak count равное 0. Во внутренней области мы создаём branch и связываем её с leaf
, после чего при печати значений счётчиков
Rc
в branch они будет иметь strong count 1 и weak count 1 (для leaf.parent указывающего на branch с
Weak
). Когда мы распечатаем счётчики из leaf
, мы увидим, что они будут иметь strong count 2, потому что branch теперь имеет клон
Rc
переменной leaf хранящийся в branch.children
, но все равно будет иметь weak count 0.
fn main
() { let leaf = Rc::new(Node { value:
3
, parent: RefCell::new(Weak::new()), children: RefCell::new(
vec!
[]),
}); println!
(
"leaf strong = {}, weak = {}"
,
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{ let branch = Rc::new(Node { value:
5
, parent: RefCell::new(Weak::new()), children: RefCell::new(
vec!
[Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!
(
"branch strong = {}, weak = {}"
,
Rc::strong_count(&branch),
Rc::weak_count(&branch),
); println!
(
"leaf strong = {}, weak = {}"
,
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
} println!
(
"leaf parent = {:?}"
, leaf.parent.borrow().upgrade()); println!
(
"leaf strong = {}, weak = {}"
,
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
Когда заканчивается внутренняя область видимости, branch выходит из области видимости и strong count
Rc
уменьшается до 0, поэтому его
Node удаляется. Weak count 1 из leaf.parent не имеет никакого отношения к тому, был ли
Node удалён,
поэтому не будет никаких утечек памяти!
Если мы попытаемся получить доступ к родителю переменной leaf после окончания области видимости, мы снова получим значение
None
. В конце программы
Rc
внутри leaf имеет strong count 1 и weak count 0 потому что переменная leaf снова является единственной ссылкой на
Rc
Вся логика, которая управляет счётчиками и сбросом их значений, встроена внутри
Rc
и
Weak
и их реализаций типажа
Drop
. Указав, что отношение из дочернего к родительскому элементу должно быть ссылкой типа
Weak
в определении
Node
,
делает возможным иметь родительские узлы, указывающие на дочерние узлы и наоборот, не создавая ссылочной зацикленности и утечек памяти.
Итоги
В этой главе рассказано как использовать умные указатели для обеспечения различных гарантий и компромиссов по сравнению с обычными ссылками, которые Rust использует по умолчанию. Тип
Box
имеет известный размер и указывает на данные размещённые в куче. Тип
Rc
отслеживает количество ссылок на данные в куче,
поэтому данные могут иметь несколько владельцев. Тип
RefCell
с его внутренней изменяемостью предоставляет тип, который можно использовать при необходимости неизменного типа, но необходимости изменить внутреннее значение этого типа; он также обеспечивает соблюдение правил заимствования во время выполнения, а не во время компиляции.
Мы обсудили также типажи
Deref и
Drop
, которые обеспечивают большую функциональность умных указателей. Мы исследовали ссылочную зацикленность,
которая может вызывать утечки памяти и как это предотвратить с помощью типа
Weak
Если эта глава вызвала у вас интерес и вы хотите реализовать свои собственные умные указатели, обратитесь к "The Rustonomicon"
за более полезной информацией.
Далее мы поговорим о параллелизме в Rust. Вы даже узнаете о нескольких новых умных указателях.
Многопоточность без страха
Безопасное и эффективное управление многопоточным программированием — ещё
одна из основных целей Rust. Многопоточное программирование, когда разные части программы выполняются независимо, и параллельное программирование, когда разные части программы выполняются одновременно, становятся всё более важными,
поскольку всё больше компьютеров используют преимущества нескольких процессоров.
Исторически программирование в этих условиях было сложным и подверженным ошибкам: Rust надеется изменить это.
Первоначально команда Rust считала, что обеспечение безопасности памяти и предотвращение проблем многопоточности — это две отдельные проблемы, которые необходимо решать различными методами. Со временем команда обнаружила, что системы владения и система типов являются мощным набором инструментов,
помогающих управлять безопасностью памяти и проблемами многопоточного параллелизма! Используя владение и проверку типов, многие ошибки многопоточности являются ошибками времени компиляции в Rust, а не ошибками времени выполнения.
Поэтому вместо того, чтобы тратить много времени на попытки воспроизвести точные обстоятельства, при которых возникает ошибка многопоточности во время выполнения,
некорректный код будет отклонён с ошибкой. В результате вы можете исправить свой код во время работы над ним, а не после развёртывания на рабочем сервере. Мы назвали этот аспект Rust бесстрашной многопоточностью. Бесстрашная многопоточность позволяет вам писать код, который не содержит скрытых ошибок и легко реорганизуется без внесения новых.
Примечание: для простоты мы будем называть многие проблемы многопоточными,
хотя более точный термин здесь — многопоточные и/или параллельные. Если бы эта книга была о многопоточности и/или параллелизме, мы были бы более конкретны.
В этой главе, пожалуйста, всякий раз, когда мы используем термин
«многопоточный», мысленно замените на понятие «многопоточный и/или
параллельный».
Многие языки предлагают довольно консервативные решения проблем многопоточности. Например, Erlang обладает элегантной функциональностью для многопоточности при передаче сообщений, но не определяет ясных способов совместного использования состояния между потоками. Поддержка только подмножества возможных решений является разумной стратегией для языков более высокого уровня, поскольку язык более высокого уровня обещает выгоду при отказе от некоторого контроля над получением абстракций. Однако ожидается, что языки низкого уровня обеспечат решение с наилучшей производительностью в любой конкретной ситуации и будут иметь меньше абстракций по сравнению с аппаратным обеспечением.
Поэтому Rust предлагает множество инструментов для моделирования проблем любым способом, который подходит для вашей ситуации и требований.
Вот темы, которые мы рассмотрим в этой главе:
Как создать потоки для одновременного запуска нескольких фрагментов кода
Многопоточность передачи сообщений, где каналы передают сообщения между потоками
Многопоточность для совместно используемого состояния, когда несколько потоков имеют доступ к некоторому фрагменту данных
Типажи
Sync и
Send
, которые расширяют гарантии многопоточности в Rust для пользовательских типов, а также типов, предоставляемых стандартной библиотекой
Использование потоков для одновременного
выполнения кода
В большинстве современных операционных систем программный код выполняется в виде процесса, причём операционная система способна управлять несколькими процессами сразу. Программа, в свою очередь, может состоять из нескольких независимых частей, выполняемых одновременно. Конструкция, благодаря которой эти независимые части выполняются, называется потоком. Например, веб-сервер может иметь несколько потоков для того, чтобы он мог обрабатывать больше одного запроса за раз.
Разбиение вычислений на несколько потоков может повысить производительность программы, поскольку программа выполняет несколько задач одновременно, но такое разбиение также добавляет сложности. Поскольку потоки могут работать одновременно,
нет чёткой гарантии, определяющей порядок выполнения частей вашего кода в разных потоках. Это может привести к таким проблемам, как:
Состояния гонки, когда потоки обращаются к данным, либо ресурсам,
несогласованно.
Взаимные блокировки, когда два потока ожидают друг друга, не позволяя тем самым продолжить работу каждому из потоков.
Ошибки, которые случаются только в определённых ситуациях, которые трудно воспроизвести и, соответственно, трудно надёжно исправить.
Rust пытается смягчить негативные последствия использования потоков, но программирование в многопоточном контексте все ещё требует тщательного обдумывания структуры кода, которая отличается от структуры кода программ,
работающих в одном потоке.
Языки программирования реализуют потоки несколькими различными способами, и многие операционные системы предоставляют API, который язык может вызывать для создания новых потоков. Стандартная библиотека Rust использует модель реализации потоков 1:1, при которой одному потоку операционной системы соответствует ровно один "языковой" поток. Существуют крейты, в которых реализованы другие модели многопоточности, отличающиеся от модели 1:1.
Создание нового потока с помощью spawn
Чтобы создать новый поток, мы вызываем функцию thread::spawn и передаём ей замыкание (мы говорили о замыканиях в главе 13), содержащее код, который мы хотим запустить в новом потоке. Пример в листинге 16-1 печатает некоторый текст из основного потока, а также другой текст из нового потока:
Файл: src/main.rs
Листинг 16-1: Создание нового потока для печати определённого текста, в то время как основной поток
печатает что-то другое
Обратите внимание, что когда основной поток программы на Rust завершается, все порождённые потоки закрываются, независимо от того, завершили они работу или нет.
Вывод этой программы может каждый раз немного отличаться, но он будет выглядеть примерно так:
Вызовы thread::sleep заставляют поток на короткое время останавливать своё
выполнение, позволяя выполняться другим потокам. Очерёдность выполнения потоков вероятно будет меняться, но это не гарантировано: это зависит от того, как ваша операционная система планирует потоки. В этом цикле основной поток печатает первым, не смотря на то, что оператор печати из порождённого потока появляется раньше в коде. И даже несмотря на то, что мы проинструктировали порождённый поток печатать до тех пор, пока значение i
не достигнет числа 9, оно успело дойти только до
5, когда основной поток завершился.
Если вы запустите этот код и увидите вывод только из основного потока или не увидите печати из других потоков, попробуйте увеличить числа в диапазонах, чтобы дать операционной системе больше возможностей для переключения между потоками.
Ожидание завершения работы всех потоков используя join use std::thread; use std::time::Duration; fn main
() { thread::spawn(|| { for i in
1 10
{ println!
(
"hi number {} from the spawned thread!"
, i); thread::sleep(Duration::from_millis(
1
));
}
}); for i in
1 5
{ println!
(
"hi number {} from the main thread!"
, i); thread::sleep(Duration::from_millis(
1
));
}
} hi number 1 from the main thread! hi number 1 from the spawned thread! hi number 2 from the main thread! hi number 2 from the spawned thread! hi number 3 from the main thread! hi number 3 from the spawned thread! hi number 4 from the main thread! hi number 4 from the spawned thread! hi number 5 from the spawned thread!
Код в листинге 16-1 преждевременно останавливает порождённый поток в большинстве случаев, из-за завершения основного потока. Более того, так как порядок выполнения потоков чётко не определён, этот код не даёт гарантии, что порождённый поток вообще начнёт исполняться!
Мы можем исправить проблему, когда созданный поток не запускается или завершается преждевременно, сохранив возвращаемое значение thread::spawn в какой-либо переменной. Тип возвращаемого значения thread::spawn
—
JoinHandle
JoinHandle
—
это владеющее значение, которое, при вызове метода join
, будет ждать завершения своего потока. Листинг 16-2 демонстрирует, как использовать
JoinHandle потока,
созданного в листинге 16-1, и вызывать функцию join
, для того, чтобы убедиться, что порождённый поток завершится раньше, чем поток main
:
Файл: src/main.rs
1 ... 39 40 41 42 43 44 45 46 ... 62