Улучшение истории параллелизма является одной из основных целей проекта Rust, поэтому следует ожидать улучшения, если мы доверяем проекту в достижении его целей. Полный отказ от ответственности: у меня высокое мнение о Rust, и я в него вложен. В соответствии с просьбой я постараюсь избегать оценочных суждений и описывать различия, а не (ИМХО) улучшения .
Безопасная и небезопасная ржавчина
«Rust» состоит из двух языков: один, который очень старается изолировать вас от опасностей системного программирования, и более мощный, без каких-либо таких устремлений.
Небезопасный Rust - это отвратительный и грубый язык, похожий на C ++. Он позволяет вам выполнять произвольно опасные вещи, общаться с оборудованием, (неправильно) управлять памятью вручную, стрелять себе в ноги и т. Д. Это очень похоже на C и C ++ в том, что правильность программы в конечном итоге в ваших руках и руки всех других программистов, вовлеченных в это. Вы выбираете этот язык с помощью ключевого слова unsafe
, и, как и в C и C ++, одна ошибка в одном месте может привести к краху всего проекта.
Safe Rust - это «по умолчанию», подавляющее большинство кода Rust безопасны, и если вы никогда не упоминаете ключевое слово unsafe
в своем коде, вы никогда не покидаете безопасный язык. Остальная часть поста будет в основном посвящена этому языку, потому что unsafe
код может нарушить все без исключения гарантии того, что Safe Rust работает так усердно, чтобы дать вам. С другой стороны, unsafe
код не является злым и не рассматривается сообществом как таковой (однако он настоятельно не рекомендуется, когда в этом нет необходимости).
Да, это опасно, но также важно, потому что позволяет создавать абстракции, используемые безопасным кодом. Хороший небезопасный код использует систему типов, чтобы предотвратить злоупотребление другими, и поэтому присутствие небезопасного кода в программе на Rust не должно нарушать безопасный код. Все следующие различия существуют, потому что в системах типов Rust есть инструменты, которых нет в C ++, и потому, что небезопасный код, который реализует абстракции параллелизма, эффективно использует эти инструменты.
Без разницы: общая / изменяемая память
Хотя Rust уделяет больше внимания передаче сообщений и очень строго контролирует совместную память, он не исключает параллелизма совместно используемой памяти и явно поддерживает общие абстракции (блокировки, атомарные операции, переменные условия, одновременные коллекции).
Более того, подобно C ++ и в отличие от функциональных языков, Rust действительно нравится традиционные императивные структуры данных. В стандартной библиотеке нет постоянного / неизменного связанного списка. Есть, std::collections::LinkedList
но это как std::list
в C ++ и не рекомендуется по тем же причинам, что и std::list
(неправильное использование кэша).
Однако, ссылаясь на заголовок этого раздела («разделяемая / изменяемая память»), Rust имеет одно отличие от C ++: он настоятельно рекомендует, чтобы память была «разделяемой XOR-изменяемой», т. Е. Чтобы память никогда не разделялась и не изменялась одновременно. время. Изменяйте память так, как вам нравится, так сказать, «в секрете вашей собственной ветки». Сравните это с C ++, где разделяемая изменяемая память является опцией по умолчанию и широко используется.
Хотя парадигма shared-xor-mutable очень важна для перечисленных ниже отличий, она также является совершенно другой парадигмой программирования, к которой нужно привыкнуть, и налагающей значительные ограничения. Иногда приходится отказываться от этой парадигмы, например, с атомарными типами ( AtomicUsize
это сущность разделяемой изменчивой памяти). Обратите внимание, что блокировки также подчиняются правилу shared-xor-mutable, поскольку оно исключает одновременное чтение и запись (в то время как один поток пишет, другие потоки не могут читать или писать).
Неразличие: гонки данных - неопределенное поведение (UB)
Если вы запускаете гонку данных в коде Rust, игра заканчивается, как в C ++. Все ставки сняты, и компилятор может делать все, что пожелает.
Тем не менее, это жесткая гарантия того, что в безопасном коде Rust нет гонок данных (или какого-либо UB в этом отношении). Это распространяется как на основной язык, так и на стандартную библиотеку. Если вы можете написать программу на Rust, которая не использует unsafe
(в том числе в сторонних библиотеках, но исключая стандартную библиотеку), которая запускает UB, то это считается ошибкой и будет исправлено (это уже происходило несколько раз). Это, если, конечно, резко контрастировать с C ++, где писать программы на UB тривиально.
Разница: строгая дисциплина блокировки
В отличие от C ++, блокировка в Rust ( std::sync::Mutex
, std::sync::RwLock
и т. Д.) Владеет данными, которые она защищает. Вместо того, чтобы брать блокировку и затем манипулировать некоторой разделяемой памятью, которая связана с блокировкой только в документации, совместно используемые данные недоступны, пока вы не удерживаете блокировку. Охрана RAII сохраняет блокировку и одновременно предоставляет доступ к заблокированным данным (это может быть реализовано в C ++, но не с помощью std::
блокировок). Пожизненная система гарантирует, что вы не сможете продолжать доступ к данным после снятия блокировки (сбросьте защиту RAII).
Конечно, вы можете иметь блокировку, которая не содержит полезных данных ( Mutex<()>
), и просто разделить некоторую память, не связывая ее явно с этой блокировкой. Однако наличие потенциально несинхронизированной общей памяти требует unsafe
.
Разница: предотвращение случайного обмена
Хотя вы можете иметь общую память, вы делитесь только тогда, когда явно просите об этом. Например, когда вы используете передачу сообщений (например, каналы от std::sync
), система времени жизни гарантирует, что вы не сохраните никаких ссылок на данные после того, как отправили их в другой поток. Чтобы поделиться данными за блокировкой, вы явно создаете блокировку и передаете ее другому потоку. Чтобы поделиться с unsafe
вами несинхронизированной памятью , ну придется использовать unsafe
.
Это связано со следующим пунктом:
Разница: отслеживание безопасности потоков
Система типов Rust отслеживает некоторые понятия безопасности потоков. В частности, эта Sync
черта обозначает типы, которые могут совместно использоваться несколькими потоками без риска состязания данных, а Send
помечает те, которые могут быть перемещены из одного потока в другой. Это обеспечивается компилятором во всей программе, и поэтому разработчики библиотек осмеливаются делать оптимизации, которые были бы глупо опасны без этих статических проверок. Например, C ++, std::shared_ptr
которые всегда используют атомарные операции для манипулирования своим счетчиком ссылок, чтобы избежать UB, если a shared_ptr
используется несколькими потоками. Rust имеет Rc
и Arc
, который отличается только тем, что Rc
использует неатомарные операции пересчета и не является поточно-ориентированным (то есть не реализует Sync
или Send
), хотя Arc
очень похож наshared_ptr
(и реализует обе черты).
Обратите внимание, что если тип не использует unsafe
ручную реализацию синхронизации, наличие или отсутствие признаков выводятся правильно.
Разница: очень строгие правила
Если компилятор не может быть абсолютно уверен, что какой-то код свободен от гонок данных и других UB, он не будет компилироваться, точка . Вышеупомянутые правила и другие инструменты могут продвинуть вас далеко вперед, но рано или поздно вам захочется сделать что-то правильное, но по незаметным причинам, которые избегают уведомления компилятора. Это может быть сложная структура данных без блокировок, но она также может быть такой же обыденной, как «Я пишу в произвольные местоположения в общем массиве, но индексы вычисляются так, что каждое местоположение записывается только одним потоком».
В этот момент вы можете либо укусить пулю и добавить немного ненужной синхронизации, либо перефразировать код так, чтобы компилятор увидел его корректность (часто выполнимую, иногда довольно сложную, иногда невозможную), или вы впадаете в unsafe
код. Тем не менее, это лишние умственные затраты, и Rust не дает вам никаких гарантий правильности unsafe
кода.
Разница: меньше инструментов
Из-за вышеупомянутых различий в Rust гораздо реже пишут код, который может иметь гонку данных (или использование после освобождения, или двойное освобождение, или ...). Хотя это хорошо, у этого есть неприятный побочный эффект, что экосистема для отслеживания таких ошибок еще более слабо развита, чем можно было бы ожидать, учитывая молодежь и небольшой размер сообщества.
Хотя такие инструменты, как valgrind и LLVM, могут в принципе применяться к коду Rust, работает ли это на самом деле, но это зависит от инструмента (и даже те из них, которые работают, могут быть сложны в настройке, тем более, что вы можете не найти какой-либо ресурсы о том, как это сделать). Это действительно не помогает, что Rust в настоящее время не хватает реальной спецификации и, в частности, формальной модели памяти.
Короче говоря, unsafe
правильно писать код Rust сложнее, чем правильно писать код C ++, несмотря на то, что оба языка примерно сопоставимы с точки зрения возможностей и рисков. Конечно, это должно быть взвешено против факта, что типичная программа на Rust будет содержать только относительно небольшую часть unsafe
кода, тогда как программа на C ++, ну, в общем, полностью C ++.
Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.
на самом деле это все еще делает даже сunsafe
элементами. Всего сырые указатели не являютсяSync
ниShare
что означает , что по умолчанию структуры , содержащие их не будут иметь ни одного .Send
илиSync
даже не должен.Ржавчина также очень похожа на Erlang и Go. Он связывается по каналам с буферами и условным ожиданием. Так же, как и Go, он ослабляет ограничения Erlang, позволяя вам совместно использовать память, поддерживать атомарный подсчет ссылок и блокировки, а также позволяет передавать каналы из потока в поток.
Однако Rust идет еще дальше. В то время как Go доверяет вам поступать правильно, Руст назначает наставника, который сидит с вами и жалуется, если вы пытаетесь поступить неправильно. Наставником Руста является компилятор. Он выполняет сложный анализ, чтобы определить владение значениями, которые передаются по потокам, и выдает ошибки компиляции, если есть потенциальные проблемы.
Ниже приводится цитата из документов RUST.
Правила владения играют жизненно важную роль в отправке сообщений, потому что они помогают нам писать безопасный параллельный код. Предотвращение ошибок в параллельном программировании - это то преимущество, которое мы получаем, когда приходится думать о том, что мы должны владеть всеми нашими программами на Rust. - Передача сообщений с владением ценностями.
Если Erlang дракониан, а Go - свободное состояние, то Rust - няня.
Вы можете найти более подробную информацию о параллелизма идеологии языков программирования: Java, C #, C, C +, Go и Rust
источник