Как позаимствовать rust
Rust начинался как проект, решающий две трудные проблемы:
- Как обеспечить безопасность (работы с памятью) в системном программировании?
- Как сделать многопоточное программирование безболезненным?
Ошибки работы с памятью и ошибки при работе с несколькими потоками частно сводятся к тому, что код обращается к некоторым данным вопреки тому, что он не должен этого делать. Секретное оружие Rust против этого — концепция владения данными, способ управления доступом к данным, которого системные программисты стараются придерживаться самостоятельно, но который Rust проверяет статически.
С точки зрения безопасности работы с памятью это означает, что вы можете не использовать сборщик мусора и в то же время не опасаться сегфолтов, потому что Rust не даст вам совершить ошибку.
Цель этого поста — показать, как это делается.
Потокобезопасность и трейт Send
Вполне логично разделять типы данных на те, которые являются "потокобезопасными", и те, которые не являются. Структуры данных, которые безопасно использовать из нескольких потоков, применяют инструменты для синхронизации внутри себя.
Например, вместе с Rust поставляется два типа "умных указателей", использующих подсчёт ссылок:
- Rc<T> , который реализует подсчёт ссылок с помощью простых операций чтения/записи. Он не является потокобезопасным.
- Arc<T> , который релизует подсчёт ссылок с помощью атомарных операций. Он является потокобезопасным.
Обычный подход сводится к тщательной документации. В большинстве языков нет семантической разницы между потокобезопасными и небезопасными типами.
В Rust всё множество типов делится на два вида — те, которые реализуют трейт Send , что означает, что эти типы можно безопасно перемещать между потоками, и те, которые его не реализуют ( !Send ), что, соответственно, значит противоположное. Если все компоненты типа являются Send , то и он сам является Send , что покрывает большинство типов. Некоторые базовые типы не являются потокобезопасными по своей сути, поэтому такие типы, как Arc , можно явно пометить как Send , что означает подсказку компилятору: "Верь мне, я обеспечил здесь всю необходимую синхронизацию".
Естественно, Arc является Send , а Rc — нет.
Мы уже видели, что Channel и Mutex работают только с Send -данными. Поскольку они являются тем самым мостиком, по которому данные перемещаются между потоками, с их помощью также и обеспечиваются гарантии, связанные с Send .
Таким образом, программисты на Rust могут пользоваться преимуществами Rc и других типов данных, небезопасных для использования в многопоточной среде, будучи уверенными, что если они попытаются случайно передать такие типы в другой поток, компилятор Rust сообщит:
Будущее
Когда Rust только создавался, каналы были встроены в язык, и в целом подход к многопоточности был довольно категоричным.
В сегодняшнем Rust'е многопоточность реализуется в библиотеках целиком. Всё, описанное в этом посте, включая Send , определено в стандартной библиотеке, и точно так же может быть реализовано в какой-нибудь другой, сторонней библиотеке.
И это очень здорово, потому что это значит, что способы работы с потоками в Rust могут всё время развиваться, предоставляя новые парадигмы и помогая в отлове новых классов ошибок. Такие библиотеки, как syncbox и simple_parallel, — это только первые шаги, и мы собираемся уделить особое внимание этой области в несколько следующих месяцев. Оставайтесь с нами!
Как позаимствовать rust
If you haven't seen my previous guide on how to move forward I recommend taking a look at that before you read this guide. To move left you have to look at your keyboard and look for the A key. A quick hint its close to the W key. After you have found.
You have 200 hours + on rust and still you cant get good . No worry my friend im here to help you :DDDD Tips: 1.Go to steam library. 2.Right click on rust. 3.Hover on manage. 4.Click Uninstall. 5.Turn off your pc. 6.Go out si.
Step 1 Create a guide like this. Step 2 Make sure that you have actual grammar to seem EPIC. Step 3 Make sure to put something about rating up and favoriting this guide. I hope you enjoyed reading the guide. Thanks for reading! Also, please mak.
Это руководство - простой калькулятор серы и взрывчатки на рейд дома. Надеюсь он будет полезным для вас.
To win in Rust shoot people and build big base to scare enemy away.
If you are not informed on how to move forward i'm going to be explaining how to do so. The first thing you are going to need to do is look at your keyboard and get familiar with the buttons. After you finish looking at your keyboard you need to look at.
heres what i have learnt over 700+ hours on rust 1. dont go afk or offline because u will get off line 2. drink lots of energy drinks because u will need it 3. spend 24+ hours then come back on to a offlined base :( (sucks) 4. if ur solo try to annoy p.
RUS: В этом руководстве я предоставлю вам количества скрапа для изучения первого, второго и третьего верстаков, а также остальных предметов. ================= ENG: In this guide, I will provide you with the amount of scrap to study the first, second and .
Make sure you type these commands in the f1 console! Commands: kill - Kills your players bind v +voice - Everytime you press v you will begin to talk in-game perf 1 - Shows FPS in-game // perf 2- Shows FPS + Ram Usage physics.steps 60 - This command.
If you ever wondered how you are getting killed while behind trees or nodes, this is the answer. Many people don't know that pistol bullets can't pierce covers like nodes, cactuses or trees, but the 5.56 ammo type can! But it depends on the angle you .
Как получить халявные очки стима. Читать далее. 1.Заходим в Steam 2.Заходим в сообщество 3.Выбираем руководство 4.Нажимаем создать руководство 5.Публикуем его.
If you ever wondered how you are getting killed while behind trees or nodes, this is the answer.
????????????????????????????????????????????????????????????????? ?????????????????????????? ????????????????????????????????????????????????????????????????? ?????????????????????????.
From 40 Scrap to 200 cloth, or 5 Sewing kits, or 10 Ropes
When processing tactical gloves, 200 fabrics are given, tactical gloves can be bought in a peaceful city for 40 scrap, and processed there in a safe zone where no one can kill you. If you decide to survive in the winter biome, you will not be able to find.
This guide will be useful for beginners and even players with many hours! In this guide it will show you how to get the Green Keycard in Rust and how / where to use it correctly! .
Основы: заимствование
Пока что код получается не очень удобным, потому что нам не нужно, чтобы print_vec уничтожал вектор, который ему передаётся. На самом деле мы бы хотели предоставить print_vec временный доступ к вектору и иметь возможность продолжить его использовать впоследствии.
Здесь нам и понадобится заимствование. В Rust если у вас есть значение, вы можете дать временный доступ к нему функциям, которые вы вызываете. Rust автоматически проверит, что эти "займы" не будут действовать дольше, чем "живёт" объект, который заимствуется.
Чтобы позаимствовать значение, нужно создать ссылку на него (ссылка — один из видов указателей) при помощи оператора & :
Теперь print_vec принимает ссылку на вектор, и use_vec отдаёт вектор "взаймы": &vec . Поскольку заимствования временные, use_vec сохраняет право владения вектором и может продолжить его использовать после того, как print_vec вернёт управление (и срок заимствования vec истёк).
Каждая ссылка действует только в определённой области видимости, которую компилятор определяет автоматически. Ссылки бывают двух видов.
- Иммутабельная ссылка &T , которая допускает совместное использование, но запрещает изменения. На одно и то же значение может быть несколько &T -ссылок, но само значение изменять нельзя до тех пор, пока эти ссылки существуют.
- Мутабельная ссылка &mut T , которая допускает изменение, но не совместное использование. Если на значение существует &mut T -ссылка, других ссылок в это время на это же самое значение быть не может, но зато значение можно изменять.
Зачем нужны два вида ссылок? Рассмотрим функцию следующего вида:
Эта функция проходит по каждому элементу вектора, помещая их все в другой вектор. В итераторе (созданном методом iter() ) содержатся ссылки на вектор в текущей и конечной позициях, и текущая позиция "перемещается" в направлении конечной.
Что произойдёт, если мы вызовем эту функцию с одним и тем же вектором в обоих аргументах?
Это приведёт к катастрофе! Когда мы помещаем новые элементы в вектор, иногда ему потребуется изменить размер, для чего выделяется новый участок памяти, в который копируются все элементы. В итераторе останется "висящая" ссылка в старую память, что приведёт к небезопасной работе с памятью, т.е. к segfault'ам или к чему-нибудь ещё похуже.
К счастью, Rust гарантирует, что пока существует мутабельное заимствование, других ссылок на объект быть не может, и поэтому код выше приведёт к ошибке компиляции:
Теперь, после того, как мы кратко рассмотрели, что такое владение и заимствование, посмотрим, как эти концепции пригождаются в многопоточном программировании.
Не общайтесь через совместный доступ к памяти; наоборот, обеспечивайте совместный доступ через общение.
— Effective Go
Владение данными в Rust позволяет очень легко преобразовать этот совет в правило, проверяемое компилятором. Рассмотрим такой API для работы с каналами (хотя каналы в стандартной библиотеке Rust немного отличаются):
Каналы — это обобщённые типы, параметризованные типом данных, которые они передают через себя (об этом говорит <T: Send> ). Ограничение Send на T означает, что T можно безопасно пересылать между потоками. Мы вернёмся к этому позднее, но пока что нам достаточно знать, что Vec<i32> является Send .
Как всегда, передача T в функцию send означает также и передачу права владения T . Отсюда следует, что вот такой код не скомпилируется:
Здесь поток создаёт вектор, отправляет его в другой поток и затем продолжает его использовать. Поток, получивший вектор, мог бы его изменить в то время, когда первый поток ещё работает, поэтому вызов print_vec мог бы привести к гонке или, например, ошибке типа use-after-free.
Вместо этого компилятор Rust выдаст ошибку на вызове print_vec :
Основы: владение данными
Мы начнём с обзора систем владения и заимствования данных в Rust. Если вы уже знакомы с ними, то вы можете пропустить обе части "основ" и перейти непосредственно к многопоточности. Если же вы захотите поглубже разобраться в этих концепциях, я очень рекомендую вот эту статью, написанную Yehuda Katz. В официальной книге Rust вы найдёте ещё более подробные объяснения.
В Rust у каждого значения есть "область владения", и передача или возврат значения означает передачу права владения ("перемещение") в новую область. Когда область заканчивается, то все значения, которыми она владеет к этому моменту, уничтожаются.
Рассмотрим несколько простых примеров. Предположим, мы создаём вектор и помещаем в него несколько элементов:
Та область видимости, в которой создаётся значение, становится его владельцем. В данном случае областью, которая владеет vec , является тело make_vec . Владелец может делать с vec всё, что угодно, в частности, менять, добавляя элементы. В конце области видимости она всё ещё владеет vec , и поэтому он автоматически уничтожается.
Становится интереснее, если вектор передаётся в другую функцию или возвращается из функции:
Теперь прямо перед окончанием области видимости make_vec , vec передаётся наружу как возвращаемое значение — он не уничтожается. Вызывающая функция, например, use_vec , получает право владения вектором.
С другой стороны, функция print_vec принимает параметр vec , и право владения передаётся в неё вызывающей функцией. Поскольку print_vec никуда дальше не передаёт право владения vec , при выходе из этой области видимости вектор уничтожается.
Как только право владения значением передано куда-то ещё, его нельзя больше использовать. Например, рассмотрим такой вариант функции use_vec :
Если вы попробуете скомпилировать этот вариант, компилятор выдаст ошибку:
Компилятор сообщает, что vec больше недоступен — право владения передано куда-то ещё. И это очень хорошо, потому что к этому моменту вектор уже уничтожен.
Изучение Rust стоит вашего времени!
Несмотря на то, что изучение займа и овладения занимает много времени, это интересно изучать. Rust попытается добиться безопасности без сбора мусора, и он до сих пор делает это очень хорошо. Некоторые люди говорят, что освоение Haskell изменяет стиль вашего программирования. Я думаю, что освоение Rust тоже стоит вашего времени.
Совместный доступ к стеку: scoped
До сих пор все структуры данных создавались на куче, которая затем использовалась из нескольких потоков. Но что если нам нужно запустить поток, который использует данные, "живущие" в стеке текущего потока? Это может быть опасно:
Дочерний поток принимает ссылку на vec , который, в свою очередь, находится в стеке parent . Когда parent возвращает управление, стек очищается, но дочерний поток об этом не знает. Ой!
Чтобы избежать подобных проблем работы с памятью, основной API для запуска потоков в Rust выглядит примерно так:
Ограничение 'static означает, грубо говоря, что в замыкании не должны использоваться заимствованные данные. В частности, это значит, что код, подобный parent выше, не скомпилируется:
По сути, это исключает возможность того, что стек parent может быть очищен, когда его ещё используют другие потоки. Катастрофа предотвращена.
Но есть и другой способ гарантировать безопасность: удостовериться, что родительский стек остаётся в порядке до тех пор, пока дочерний поток не завершится. Такой паттерн называется fork-join-программированием и часто применяется при разработке параллельных алгоритмов типа "разделяй и властвуй". Rust поддерживает этот подход с помощью специальной функции для запуска дочернего потока:
У этого API два ключевых отличия от spawn , описанного выше.
- Использование параметра 'a вместо 'static . Этот параметр обозначает область видимости, которая является верхней границей всех заимствований внутри замыкания f .
- Наличие возвращаемого значения, JoinGuard . Как подсказывает его название, JoinGuard гарантирует, что родительский поток присоединяется к дочернему потоку (ждёт его), неявно выполняя операцию присоединения в деструкторе (если она ещё не была выполнена явно).
Таким образом, в Rust вы можете свободно использовать данные, размещённые на стеке, в дочерних потоках, будучи уверенными, что компилятор проверит наличие всех необходимых операций синхронизации.
Примечание переводчика. Буквально в тот же день, когда вышла эта статья, была обнаружена возможность нарушить гарантии, предоставляемые scoped , в безопасном коде. Из-за этого функция thread::scoped была экстренно дестабилизирована, поэтому её нельзя использовать с бета-версией компилятора, а только с nightly. Эту проблему планируется так или иначе починить к релизу 1.0.
Почему владение/заимствование в Rust такое сложное?
Оригинал статьи написан живущим на вашингтонщине Иваном Сагалаевым, мужем небезызвестной Алёны C++.
Сама статья.
Работать с чистыми функциями просто: вы передаете аргументы и получаете результат, при этом нет никаких побочных эффектов. С другой стороны, если функция производит побочные эффекты, такие, как изменение собственных аргументов или же глобальных объектов, то найти причины этого трудно. Мы привыкли также, что если видим что-то вроде player.set_speed(5), то можно быть уверенным, что тут собираются изменить объект player предсказуемым способом (и, возможно, посылают некоторые сигналы куда-нибудь).
Система владения/заимствования языка Rust сложна и она создает совершенно новый класс побочных эффектов.
Простой пример
Рассмотрим этот код:
Опыт большинства программистов не подготовит к тому, что объект point вдруг становится недоступным после вызова функции is_origin()! Компилятор не позволит вам использовать его в следующей строке. Это – побочный эффект, что-то произошло с аргументом, но это совсем не то, что вы видели в других языках.
Пример посложнее
Рассмотрите парсер, который берет некоторые данные из lexer и сохраняет некоторое состояние:
Ненужная на первый взгляд consume_lexeme() является просто удобной оберткой вокруг длинной последовательности вызовов, которые я делаю в приведённом коде. lexer.next() возвращает самодостаточную лексему путем копирования данных из внутреннего буфера lexer. Но теперь мы хотим это оптимизировать, чтобы лексемы содержали только ссылки на эти данные во избежание копирования. Меняем объявление метода на следующее:
Пометка 'a явно нам говорит, что время жизни лексемы теперь связано с временем жизни ссылки на lexer, с которой мы вызываем метод .next(). Т.е. не может жить само по себе, а зависит от данных в буфере lexer. И теперь Parser::next() перестает работать:
Проще говоря, компилятор Rust говорит нам, что до тех пор, пока lexeme доступна в этом блоке кода, он не позволит нам изменить self.state – другую часть парсера. Но это вообще бессмысленно! Виновником здесь является consume_lexeme(). Хотя на самом деле нам нужен только self.lexer, мы говорим компилятору, что ссылаемся на весь парсер (обратите внимание на self). И поскольку эта ссылка может быть изменена, компилятор никому не позволит касаться любой части парсера для изменения данных, зависящих теперь от lexeme. Таким образом, мы имеем побочный эффект снова: хотя мы не меняли фактические типы в сигнатуре функции и код по-прежнему правилен и должен работать корректно, смена собственника неожиданно не позволяет ему далее компилироваться.
Даже при том, что я понял проблему в целом, мне потребовались не менее чем два дня, чтобы до меня дошло и исправление стало очевидным.
Исправляем
Изменение consume_lexeme(), позволяющее ссылаться только на lexer, а не на весь парсер, устранило проблему, но код не выглядел идиоматически из-за замены нотации с точкой на вызов обычной функции:
К счастью, Rust тоже позволяет пойти по правильному пути. Поскольку в Rust определение полей данных (struct) отличаются от определения методов (impl), я могу определить мои собственные методы для любой структуры, даже если он импортируется из другого пространства имен:
Проверка заимствований в Rust – это замечательная вещь, которая заставляет вас писать более надежный код. Но это отличается от того, к чему вы привыкли, и потребуется время для развития навыков эффективной работы.
Отзывы читателей
Juarez: У меня сложилось впечатление, что Rust добавляет излишнюю сложность внедрением «перемещение по умолчанию» для элементарных типов. Программист везде имеет дополнительное бремя boxing-ссылок. На мой взгляд, кажется, естественно думать о:
а) «копирование по умолчанию» для элементарных типов
б) «ссылка по умолчанию» для составных типов (структуры, трейты и т.д.)
в) «перемещение по умолчанию» для составных типов в асинхронных методах – от случая к случаю.
Я что-то пропустил?
Ralf: Обратите внимание, однако, что то, что вы называете «эффект» здесь на самом деле очень, очень сильно отличается от тех «эффектов», которые люди обычно имеют в виду, когда они говорят о «побочных эффектах». Понятия владения и перемещения является понятиями только время компиляции, оно не меняет того, что делает ваш код. Следовательно, это не делает рассуждения о поведении вашего кода сложнее, так как поведение не изменяется.
На самом деле побочные эффекты теперь гораздо более управляемы. Это относится, в частности, к рассуждения о неограниченных эффектах, подобным тем, которые имеет C++, где почти везде можно получить доступ ко всем видам данных под псевдонимами.
Обязанность проверки заимствования и владения – не новый побочный эффект, речь идет об ограничении существующих побочных эффектов. Если вы владеете чем-то или имеете изменяемую ссылку (которая обязательно является уникальной), вы можете быть уверенными в отсутствии неожиданных (нелокальных) побочных эффектов этого объекта, потому что никто не может иметь его псевдоним. Под этим я имею в виду, что вызов некоторой функции для некоторых данных (которыми вы владеете) никогда не будет их изменять волшебным образом. Если у вас есть разделяемая ссылка, вы можете быть уверены в отсутствии побочных эффектов, потому что никто не может изменять данные. Когда компилятор говорит вам, что данные перемещаются и вы не можете их использовать, это не новый побочный эффект. Это «простое» понимание компилятором побочных эффектов нужно для того, чтобы он мог убедиться, что всё находятся под контролем.
В C++, если вы передаёте параметр Point перед некоторой функцией, компилятор делает неполную копию, и если Point содержит указатели, то это может привести к беспорядку. Здесь объект Point является безопасным для копирования в близлежащем контексте, но вы должны явно сказать компилятору, что вы хотите:
Вы можете задаться вопросом, почему компилятор не может понять это автоматически. Было бы точно так же, как это делается для Send.
Проблемой здесь является стабильность интерфейса. Если вы пишете библиотеку, которая экспортирует тип, к которому применяется Copy, то библиотека обязана всегда сохранять этот тип Copy и в будущем. Он должен быть осознанным выбором автора библиотеки, чтобы гарантировать, что этот тип является и всегда будет являться Copy – вследствие явной аннотации.
Послесловие от переводчика: стимулом к переводу этой статьи было желание узнать, что же за «совершенно новый класс побочных эффектов» возник в Rust. Хотя в целом статья любопытна, автор находится в некотором заблуждении насчёт совершенно нового класса.
Область заимствования
Всё становится интересным, если мы передадим сюда ссылки (& и &mut), и тут многие новички начинают путаться.
Время существования
Во всей истории заимствования важно знать, где заимствование заёмщика начинается и где оканчивается. В Руководстве по времени существования это называется временем существования:
Время существования — это статическая оценка расстояния выполнения, в процессе которого указатель действителен: оно всегда соответствует выражению или блоку внутри программы.
& = заимствование
Пару слов о заимствовании. Во-первых, просто запомните, что & = заимствование, а &mut = изменяемое заимствование. Где бы вы не увидели символ & — это заимствование.
Во-вторых, если символ & показывается в каждой структуре (в её поле) или в функции/замыкании (в его возвращаемом типе или захваченным ссылкам), то такая структура/функция/замыкание является заёмщиком, и к ней применяются все правила заимствования.
В-третьих, для каждого заимствования имеется владелец и одиночный заёмщик или множественные заёмщики.
Расширение области займа
Несколько слов об области займа. Во первых, область займа:
— это область где заимствование эффективно, и
— заёмщик может расширить область займа (см ниже).
Во-вторых, заёмщик может расширить область займа через копию (неизменяемое заимствование) или переместить (изменяемый заём), что происходит в присваиваний или в вызовах функций. Приёмник (это может быть новая связь, структуры, функции или замыкание)далее становится новым заёмщиком.
В-третьих, область займа является объединением из областей всех заёмщиков, а заимствованный ресурс должен быть действителен в течение всего области заёма.
Формула займа
Теперь, у нас есть формула займа:
область ресурсов >= область займа = объединение областей всех заёмщиков
Пример кода
Давайте взглянем на некоторые примеры расширения области займа. Структура struct Foo та же самая как и прежде:
Даже несмотря на то, что заём происходит внутри if блока, и заёмщик х выходит за рамки после if блока, он расширил сферу заимствования через присваивания y = x;, так что есть два заёмщика: х и у. В соответствии с формулой заёма, область заёма является объединением заёмщика х и заёмщика у, которое находится между первым заёмом let x = &a; и до конца основного блока. (Обратите внимание, что связывание let y: &Foo; не заёмщик)
Вы, возможно, заметили, что, блок if никогда не будет выполнен, так как условие всегда ложно, но компилятор всё ещё запрещает владельцу ресурса `a` доступ к ресурсу. Это потому, что все проверки займа происходят во время компиляции, во время выполнения ничего не сделаешь.
Заимствование и время существования в Rust
Rust — это новый язык программирования, находящийся в активной разработке с версии 1.0. Я могу написать другой блог о Rust и том, почему он крут, но сегодня я сфокусируюсь на его системе заимствования и времени существования, которая запутывает многих новичков, в том числе и меня самого. Данный пост предполагает, что у вас есть базовое понимание Rust. Если же нет, вы можете сперва прочитать само Руководство и Руководство по указателям.
Заимствование нескольких ресурсов
До сих пор мы сосредоточивались только на заимствованиях из одного ресурса. Может ли заёмщик брать несколько ресурсов? Конечно! Например, функция может принимать две ссылки и возвращать одного из них в зависимости от определенных критериев, например, кто из ссылок больше другого:
Функция max возвращает указатель &, следовательно, это заемщик. Возвратный результат может быть любым из входящих ссылок, поэтому он заимствует два ресурса.
Именованная область займа
При наличии нескольких указателей & в качестве входящих, мы должны указать их отношения с помощью времени жизней с именем, определенным в руководстве времени существования. Но сейчас давайте просто называть их именованной областью займа.
Приведенный выше код не будет принят компилятором без указания отношения между заёмщиками, то есть, те заёмщики, которые сгруппированы в их области займа. Вот эта реализация будет правильная:
В этой функции у нас есть одна область займа 'a' и три заемщика: два входных параметра и возвращаемый функцией результат. Вышеупомянутая формула заимствования всё ещё применяется, но теперь каждый заимствованый ресурс должен удовлетворять формуле. Смотрите пример ниже.
Пример кода
Давайте использовать функцию max в следующем коде, чтобы выбрать самое больше из a и b:
До let x = max(&a, &b); всё хорошо, потому, что &a и &b — это временные ссылки, которые действительны только в выражении, а третий заёмщик х заимствует два ресурса (либо a либо b но с проверкой заёмщика, если заимствованы оба) до конца блока if, таким образом, область займа находится в let x = max(&a, &b); до конца блока if. Ресурсы a и b действительны по всей области займа, следовательно, удовлетворяют формуле займа.
Теперь, если мы раскомментируем последнее значение y = x;, y станет четвертым заёмщиком, а область займа увеличится до конца основного блока, в результате чего ресурс b провалить тест формулы займа.
Блокировки
Другой способ работы со многими потоками — это организация общения потоков через пассивное разделяемое состояние.
У многопоточности с разделяемым состоянием дурная слава. Очень легко забыть захватить блокировку или как-то ещё изменить не те данные не в то время, с катастрофичным результатом — настолько легко, что многие программисты отказываются от такого способа многопоточного программирования полностью.
Подход Rust заключается в следующем:
- Многопоточность с разделяемым состоянием так или иначе является фундаментальным стилем программирования, необходимым для системного кода, максимальной производительности и для реализации других стилей многпоточного программирования.
- На самом деле, проблема заключается в случайно разделяемом состоянии.
Потоки в Rust "изолированы" друг от друга автоматически благодаря концепции владения данными. Запись может происходить только тогда, когда у потока есть мутабельный доступ к данным: либо за счёт того, что поток ими владеет, либо за счёт наличия мутабельной ссылки. Так или иначе, гарантируется, что поток будет единственным, кто в данный момент времени может получить доступ к данным. Рассмотрим реализацию блокировок в Rust, чтобы понять, как это работает.
Вот упрощённая версия их API (вариант в стандартной библиотеке более эргономичен):
Этот интерфейс достаточно необычен в нескольких аспектах.
Во-первых, у типа Mutex есть типовый параметр T , означающий данные, защищаемые этой блокировкой. Когда вы создаёте мьютекс, вы передаёте ему право владения данными, немедленно теряя к ним доступ. (После создания блокировки остаются в незахваченном состоянии)
Далее, вы можете использовать функцию lock , чтобы заблокировать поток до тех пор, пока он не захватит блокировку. Особенность этой функции в том, что она возвращает специальное значение-предохранитель, MutexGuard<T> . Этот объект автоматически отпускает блокировку после своего уничтожения — отдельной функции unlock здесь нет.
Единственным способом получить доступ к данными является функция access , которая превращает мутабельную ссылку на предохранитель в мутабельную ссылку на данные (с меньшим временем жизни):
Здесь мы можем отметить два ключевых момента:
- мутабельная ссылка, которая возвращается функцией access , не может действовать дольше, чем MutexGuard , из которого она получена;
- блокировка отпускается, только когда MutexGuard уничтожается.
Компилятор Rust сгенерирует ошибку, в точности указывающую на проблему:
Почему время существования сбивает с толку
Наконец, я хочу объяснить, почему я думаю, что термин время существования, используемый системой займа Rust’а вносит путаницу (и, таким образом, избежать использования термина в этом блоге).
Когда мы говорим о займе, есть три вида «времени существования»:
А: время существования владельца ресурса (или владеющий/заимствованный ресурс)
В: «время существования» всего займа, т.е. с первого займа и до возвращения
С: время существования отдельного заёмщика или заимствованного указателя
Когда кто-то говорит о термине «время существования», он может иметь ввиду любое из выше перечисленных. Если участвуют ещё и несколько ресурсов и заёмщиков, то всё становится еще более запутанным. Например, что делает «время жизни с именем» в объявлении функции или структуры? Означает ли это А, В или С?
В нашем предыдущей функции max:
Что означает время существования 'a'? Это не должно быть А, поскольку два ресурса задействованы и имеют разные времена существования. Это не может быть и С, потому, что есть три заёмщика: х, y и возвращаемое значение функции и все они имеют разные времена жизни. Означает ли это B? Вероятно. Но вся область заёма не является конкретным объектом, как оно может иметь «время существования»? Назвать это временем существования — значит заблуждаться.
Кто-то может сказать, что это означает минимальные требования времени существования для заимствованных ресурсов. В некоторых случаях, это может иметь значение, однако как мы можем называть их «временем существования»?
Понятие собственности/заимствования само по себе сложное. Я бы сказал, что путаница в том, что даёт «время жизни», делает освоение еще более непонятным.
P.S. С использованием A, B и C, определенный выше, формула займа становится:
Структура как заёмщик
В дополнение к функциям и замыканиям, структура может также занимать несколько ресурсов, сохраняя несколько ссылок в своей области(ях). Посотрим на пример ниже, и как применяется формула займа. Давайте использовать структуру Link для хранения ссылки(неизменяемый займ(immutable borrow)):
Структура заимствует несколько ресурсов
Даже только с одним полем, структура Link может занять несколько ресурсов:
В приведенном выше примере, заёмщик х заимствовует ресурсы от владельца a, и область займа идет до конца основного блока. Всё идёт нормально. Если мы раскомментируем последнюю строку x.link = &b;, x также попытается заимствовать ресурс у владельца b, и тогда ресурс b провалит тест на формулу займа.
Функция для расширения области займа без возвращаемого значения
Функция без возвращаемого значения может также расширить область займа через его входные параметры. Например, функция store_foo принимает изменяемую ссылку на Link, и сохраняет в нее ссылку Foo(immutable borrow):
В следующем коде, заимствованные ресурсы овладели ресурсами; Структура Link изменяемо ссылается на заемщик х (т.е. *х является заемщиком); Область займа идет до конца основного блока.
Если мы раскомментирем последнюю строку store_foo(x, &b); функция попытается хранить &b в x.link, делая ресурс b другим заимствованным ресурсом и провалит тест формулы займа, так как область ресурса b не покрывает всю область займа.
Несколько областей займа
Функция может иметь несколько именованных областей займа. Например:
В этой (вероятно, не очень полезной) функции участвуют две разрозненные области займа. Каждая область займа будет иметь свою собственную формулу заимствования.
Владение ресурсами и заимствование
В Rust безопасность по памяти обеспечивается без сбора мусора путём использования усложнённой системы заимствования. Имеется как минимум один владелец (owner) для любого ресурса, который занимается освобождением своих ресурсов. Вы можете создать новые биндинги, чтобы обратиться к ресурсу, использующему & или &mut, которые называются заимствованием (borrow) и изменяемым заимствованием (mutable borrow). Компилятор следит за должным поведением всех владельцев (owners) и заёмщиков (borrowers).
Копирование и перемещение
Перед тем, как перейти к системе заимствования, нам нужно знать, как методы copy и move обрабатываются в Rust. Этот ответ из StackOverflow просто необходимо прочитать. В целом, в присваиваниях и в функции они вызываются:
1. Если значение копируемо ( с участием лишь примитивных типов, без участия ресурсов, например обработки памяти или файла), компилятор по умолчанию копирует.
2. В противном случае, компилятор перемещает (передает) владение (ownership) и делает недействительным оригинальный биндинг.
Вкратце, pod (читаемые старые данные) => copy, non-pod (линейные типы) => move.
Тут есть несколько дополнительных замечаний для справки:
* Метод Rust copy похож на Си. Каждое использование по значению является побайтовым копированием (теневой метод memcpy copy) вместо семантического копирования или клонирования.
* Чтобы сделать структуру pod некопируемой, вы можете использовать поле маркера NoCopy или реализовать типаж Drop.
После перемещения, владение передаётся следующему владельцу.
Освобождение ресурса
В Rust любой объект освобождается как только его владение исчезнет, например когда:
1. Владелец окажется вне области или
2. Владение биндингом изменяется (тем самым, оригинальный биндинг становится void)
Привилегии и ограничения владельца (owner) и заёмщика (borrower)
Этот раздел основывается на Руководстве по Rust с упоминанием методов copy и move в части привилегий.
Владелец имеет некоторые привилегии. И может:
1. Контролировать деаллокацию ресурса
2. Занимать ресурс неизменяемо (множественные заимствования) или изменяемо (эксклюзивно) и
3. Передавать владение (с перемещением).
Владелец также имеет некоторые ограничения:
1. В процессе заимствования, владелец не может (а) изменять ресурс или (б) занимать его в измененном виде.
2. В процессе изменяемого заимствования, владелец не может (а) иметь доступ к ресурсу или (б) занимать его.
Заёмщик тоже имеет некоторые привилегии. В дополнение к получению доступа или изменению заимствованного ресурса, заёмщик также может делиться другим заёмщиком:
1. Заёмщик может распределить (копировать) указатель неизменяемое заимствование (immutable borrow)
2. Изменяемый заёмщик может передавать (перемещать) изменяемое заимствование. (Заметьте, что изменяемая ссылка (mutable reference) перемещена).
Примеры кода
К каждому образцу кода также даётся «диаграмма области» для иллюстрации областей владельца, заёмщиков и т. д. Фигурные скобки в строке заголовка совпадают с фигурными скобками в самом коде.
Владелец не может иметь доступ к ресурсу в процессе изменяемого заимствования
Нижеследующий код не скомпилируется, если мы не раскоментируем последнюю строку «println:»:
Заёмщик может перемещать изменяемое заимствование в новый заёмщик
После перемещения, оригинальный заёмщик x больше не имеет доступа к заимствованному ресурсу.
Гонки данных
Теперь мы рассмотрели достаточно примеров, чтобы привести, наконец, довольно строгое утверждение о подходе Rust к многопоточности: компилятор предотвращает все гонки данных.
Гонка данных (data race) возникает при несинхронизированном обращении к данным из нескольких потоков, при условии, что как минимум одно из этих обращений является записью.
Под синхронизацией здесь подразумеваются такие инструменты, как низкоуровневые атомарные операции. Фактически, утверждение о предотвращении всех гонок данных — это такой способ сказать, что вы не сможете случайно "поделиться состоянием" между потоками. Любое обращение к данным, включающее их изменение, должно обязательно проводиться с использованием какой-нибудь формы синхронизации.
Гонки данных — это только один (хоть очень важный) пример состояния гонки, но, предотвращая их, Rust помогает избежать других, скрытых форм гонок. Например, бывает важно обеспечить атомарность обновления одновременно нескольких участков памяти: другие потоки "увидят" либо все обновления сразу, либо ни одно из них. В Rust наличие ссылки типа &mut на все соответствующие области памяти в одно и то же время гарантирует атомарность их изменений, потому что ни один другой поток не сможет получить к ним доступ на чтение.
Стоит остановиться на секунду, чтобы осмыслить эту гарантию в контексте всего множества языков программирования. Многие языки предоставляют безопасность работы с памятью с помощью сборщика мусора, но сборка мусора не помогает предотвращать гонки данных.
Вместо этого Rust использует владение данными и заимствования для реализации своих двух ключевых положений:
- безопасность работы с памятью без сборки мусора;
- многопоточность без гонок данных.
Читайте также: