Улучшение истории параллелизма является одной из основных целей проекта 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 ++.