Как Rust расходится с возможностями параллелизма в C ++?

35

Вопросов

Я пытаюсь понять, улучшает ли Rust фундаментально и достаточно средства параллелизма в C ++, чтобы решить, стоит ли мне тратить время на изучение Rust.

В частности, как идиоматический Rust улучшается или, во всяком случае, расходится с возможностями параллелизма идиоматического C ++?

Является ли улучшение (или расхождение) в основном синтаксическим, или это существенно улучшение (расхождение) в парадигме? Или что-то еще? Или это не совсем улучшение (расхождение) вообще?


обоснование

Недавно я пытался научить себя средствам параллелизма в C ++ 14, и что-то кажется мне не совсем правильным. Что-то чувствует себя плохо. Что чувствует? Сложно сказать.

Мне кажется, что компилятор на самом деле не пытался помочь мне написать правильные программы, когда дело доходит до параллелизма. Такое ощущение, что я использовал ассемблер, а не компилятор.

По общему признанию, вполне вероятно, что я все еще страдаю от тонкой, ошибочной концепции, когда дело доходит до параллелизма. Возможно, я еще не испытываю напряжения Бартоша Милевского между программированием с сохранением состояния и гонками за данными. Может быть, я не совсем понимаю, как много звука в параллельной методологии находится в компиляторе, а сколько в ОС.

THB
источник

Ответы:

56

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


источник
6
Где на моем экране находится переключатель +25 upvote? Я не могу найти это! Этот информативный ответ высоко ценится. Это оставляет меня без очевидных вопросов по вопросам, которые он охватывает. Итак, по другим вопросам: если я понимаю документацию Rust, у Rust есть [a] интегрированные средства тестирования и [b] система сборки под названием Cargo. По вашему мнению, они достаточно готовы к производству? Кроме того, что касается Cargo, хорошо ли мне разрешать добавлять в процесс сборки оболочку, скрипты Python и Perl, компиляцию LaTeX и т. Д.?
THB
2
@thb Тестирование очень простое (например, без насмешек), но функционально. Cargo работает довольно хорошо, хотя его фокус на Rust и воспроизводимости означает, что он может быть не лучшим вариантом для прохождения всех этапов от исходного кода до конечных артефактов. Вы можете писать сценарии сборки, но это может не подходить для всех вещей, которые вы упоминаете. (Тем не менее, люди регулярно используют сценарии сборки для компиляции библиотек C или поиска существующих версий библиотек C, так что это не значит, что Cargo перестает работать, когда вы используете больше, чем чистый Rust.)
2
Между прочим, ваш ответ выглядит довольно убедительным. Так как мне нравится C ++, так как C ++ имеет достойные возможности для почти всего, что мне нужно сделать, поскольку C ++ является стабильным и широко используемым, я до сих пор был вполне удовлетворен тем, чтобы использовать C ++ для всех возможных несложных целей (у меня никогда не было интереса к Java , например). Но теперь у нас есть параллелизм, и мне кажется, что C ++ 14 борется с ним. Я добровольно не пробовал новый язык программирования в течение десятилетия, но (если Хаскель не должен показаться лучшим вариантом), я думаю, что мне придется попробовать Rust.
THB
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что означает , что по умолчанию структуры , содержащие их не будут иметь ни одного .
Хаулет
@ ŁukaszNiemier Может случиться, что все получится, но есть миллиард способов, которыми небезопасный тип использования может оказаться Sendили Syncдаже не должен.
-2

Ржавчина также очень похожа на Erlang и Go. Он связывается по каналам с буферами и условным ожиданием. Так же, как и Go, он ослабляет ограничения Erlang, позволяя вам совместно использовать память, поддерживать атомарный подсчет ссылок и блокировки, а также позволяет передавать каналы из потока в поток.

Однако Rust идет еще дальше. В то время как Go доверяет вам поступать правильно, Руст назначает наставника, который сидит с вами и жалуется, если вы пытаетесь поступить неправильно. Наставником Руста является компилятор. Он выполняет сложный анализ, чтобы определить владение значениями, которые передаются по потокам, и выдает ошибки компиляции, если есть потенциальные проблемы.

Ниже приводится цитата из документов RUST.

Правила владения играют жизненно важную роль в отправке сообщений, потому что они помогают нам писать безопасный параллельный код. Предотвращение ошибок в параллельном программировании - это то преимущество, которое мы получаем, когда приходится думать о том, что мы должны владеть всеми нашими программами на Rust. - Передача сообщений с владением ценностями.

Если Erlang дракониан, а Go - свободное состояние, то Rust - няня.

Вы можете найти более подробную информацию о параллелизма идеологии языков программирования: Java, C #, C, C +, Go и Rust

srinath_perera
источник
2
Добро пожаловать в Stack Exchange! Обратите внимание, что всякий раз, когда вы ссылаетесь на свой собственный блог, вы должны указать это явно; см. справочный центр .
Глорфиндель