Язык программирования Rust
Скачать 7.02 Mb.
|
Создание древовидной структуры данных: 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: [] } }] } }) Листинг 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 |