Як Rust розходиться з умовами одночасності C ++?


35

Запитання

Я намагаюся зрозуміти, чи принципово і достатньо вдосконалюється Раст на умовах сумісності C ++, щоб вирішити, чи варто витрачати час на вивчення Іржі.

Зокрема, як ідіоматичний Іржа покращується на будь-якому рівні, або в будь-якому випадку відхиляється від сумісних засобів ідіоматичного C ++?

Чи є поліпшення (або розбіжність) переважно синтаксичним, чи це суттєво поліпшення (розбіжність) парадигми? Або це щось інше? Або насправді це зовсім не поліпшення (розбіжність)?


Обґрунтування

Нещодавно я намагався навчити себе паралельно з конкурентними можливостями C ++ 14, і щось почувається не зовсім правильно. Щось відчувається. Що відчуває себе? Важко сказати.

Схоже, що компілятор насправді не намагався допомогти мені написати правильні програми, коли мова йде про одночасність. Схоже, я використовував асемблер, а не компілятор.

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

Відповіді:


56

Краща історія паралельності є однією з головних цілей проекту «Іржа», тому слід очікувати вдосконалення за умови довіри проекту до досягнення його цілей. Повна відмова від відповідальності: Я високо оцінюю Расту і я в неї інвестую. У відповідності з проханням, я буду намагатися оціночними судженнями уникати і описати відмінності , а не (ИМХО) поліпшень .

Безпечна і небезпечна іржа

"Іржа" складається з двох мов: тієї, яка дуже намагається ізолювати вас від небезпеки системного програмування, і більш потужної без будь-яких подібних прагнень.

Небезпечна іржа - це бридка, британська мова, яка дуже відчуває себе як C ++. Це дозволяє робити довільно небезпечні речі, розмовляти з технікою, (неправильно) керувати пам’яттю вручну, стріляти собі в ногу і т. Д. Це дуже схоже на C і C ++ в тому, що правильність програми в кінцевому рахунку у ваших руках і руки всіх інших програмістів, які беруть участь у цьому. Ви ввімкнули цю мову за допомогою ключового слова unsafe, і, як і в C та C ++, одна помилка в одному місці може призвести до того, що весь проект вийде з ладу.

Безпечна іржа - це "за замовчуванням", переважна більшість коду Rust є безпечною, і якщо ви ніколи не згадуєте ключове слово unsafeу своєму коді, ви ніколи не залишаєте безпечну мову. Інша частина публікації стосуватиметься переважно цієї мови, оскільки unsafeкод може порушити будь-які гарантії того, що безпечний Rust так важко дає вам. З іншого боку, unsafeкод не є злим і не сприймається громадою як такий (він, однак, сильно не рекомендується, коли це не потрібно).

Це так, але це також важливо, оскільки дозволяє будувати абстракції, які використовує безпечний код. Хороший небезпечний код використовує систему типу, щоб запобігти зловживанням ним інших людей, а тому наявність небезпечного коду в програмі Rust не повинна порушувати безпечний код. Усі наступні відмінності існують через те, що в системах типу Руста є інструменти, яких у C ++ немає, і через те, що небезпечний код, який реалізує абстракцію паралельності, ефективно використовує ці інструменти.

Без різниці: спільна / змінна пам'ять

Хоча Руст приділяє більше уваги передачі повідомлень і дуже суворо керує спільною пам'яттю, вона не виключає одночасності спільної пам'яті і явно підтримує загальні абстракції (блокування, атомні операції, змінні стану, одночасні колекції).

Більше того, як C ++ і на відміну від функціональних мов, Руст дуже любить традиційні імперативні структури даних. У стандартній бібліотеці немає стійкого / незмінного пов'язаного списку. Є, std::collections::LinkedListале це як std::listу C ++ і відстороняється з тих же причин, що і std::list(неправильне використання кешу).

Однак, посилаючись на заголовок цього розділу ("спільна / змінна пам'ять"), Rust має одну різницю на C ++: Це настійно рекомендує пам'яті бути "спільним XOR-змінним", тобто, що пам'ять ніколи не поділяється і не змінюється одночасно. час. Вимкніть пам'ять, як вам подобається, "у конфіденційності власної нитки", так би мовити. Порівнюйте це зі C ++, де спільна мутаційна пам'ять є типовим варіантом і широко використовується.

Незважаючи на те, що парадигма розділяється на xor-мутацій є дуже важливою для наведених нижче відмінностей, це також зовсім інша парадигма програмування, яка потребує певного часу, щоб звикнути, і це ставить значні обмеження. Інколи доводиться відмовлятися від цієї парадигми, наприклад, з атомними типами ( AtomicUsizeце суть спільної змінної пам'яті). Зауважте, що блоки також підкоряються правилу спільного xor-mutable, оскільки воно виключає одночасне зчитування та записування (поки пише один потік, жоден інший потік не може читати чи записувати).

Без різниці: перегони даних - це не визначена поведінка (UB)

Якщо ви запускаєте перегони даних у коді Rust, це закінчується грою, як і в C ++. Усі ставки відключені, і компілятор може робити все, що заманеться.

Однак, це гарантія, що безпечний код Rust не має перегонів даних (або будь-якого UB з цього питання). Це поширюється як на основну мову, так і на стандартну бібліотеку. Якщо ви можете написати програму Rust, яка не використовує unsafe(в тому числі в сторонніх бібліотеках, але виключає стандартну бібліотеку), яка запускає UB, то це вважається помилкою і буде виправлено (це вже траплялося кілька разів). Це, звичайно, на відміну від C ++, де писати програми з UB тривіально.

Різниця: Сувора дисципліна блокування

В відміну від C ++, замок в Руста ( std::sync::Mutex, std::sync::RwLock, і т.д.) має дані , які він захищають. Замість того, щоб брати замок і потім маніпулювати деякою спільною пам'яттю, яка асоціюється з блокуванням лише в документації, спільні дані недоступні, поки ви не тримаєте замок. Охоронець RAII зберігає блокування і одночасно надає доступ до заблокованих даних (це багато що може бути реалізовано C ++, але не за допомогою std::блокування). Система експлуатації гарантує, що ви не можете продовжувати отримувати доступ до даних після звільнення блокування (скиньте захист RAII).

Звичайно, ви можете мати замок, який не містить корисних даних ( Mutex<()>), і просто поділитися деякою пам’яттю, не пов'язуючи його з цим замком. Однак наявність потенційно несинхронізованої спільної пам'яті вимагає unsafe.

Різниця: Запобігання випадковому обміну

Хоча ви можете мати спільну пам'ять, ви ділитесь лише тоді, коли явно про це просите. Наприклад, коли ви використовуєте повідомлення, що передаються (наприклад, канали від std::sync), система експлуатації гарантує, що ви не збережете посилання на дані після того, як ви надіслали їх в інший потік. Щоб ділитися даними за блоком, ви чітко будуєте замок і надаєте їх іншому потоку. Щоб поділитися з unsafeвами несинхронізованою пам’яттю , доведеться використовувати unsafe.

Це пов'язано з наступним моментом:

Різниця: відстеження безпеки нитки

Система типу Руста відстежує деяке поняття безпеки ниток. Зокрема, Syncознака позначає типи, які можуть бути спільними для декількох потоків без ризику перебігу даних, тоді як Sendпозначає ті, які можна переміщувати з одного потоку в інший. Це застосовується компілятором у всій програмі, і таким чином дизайнери бібліотек наважуються робити оптимізацію, яка була б нерозумно небезпечною без цих статичних перевірок. Наприклад, C ++, std::shared_ptrякий завжди використовує атомні операції, щоб маніпулювати своїм контрольним числом, щоб уникнути UB, якщо shared_ptrтрапляється використовувати декілька потоків. Іржа має Rcі Arcвідрізняється лише тим, що Rc використовує неатомні операції перезавантаження і не є безпечною для потоків (тобто не реалізує Syncабо Send), хоча Arcдуже схожаshared_ptr (і реалізує обидві ознаки).

Зауважте, що якщо тип не використовується unsafeвручну для синхронізації, наявність чи відсутність ознак визначається правильно.

Різниця: Дуже суворі правила

Якщо компілятор не може бути абсолютно впевнений, що якийсь код не містить перегонів даних та інших UB, він не буде компілюватися, періодично . Вищезазначені правила та інші інструменти можуть доставити вас далеко, але рано чи пізно ви захочете зробити щось правильне, але з тонких причин, що уникають повідомлення компілятора. Це може бути хитрою структурою даних без блокування, але вона може бути настільки ж повсюдною, як "Я записую у випадкові місця у спільному масиві, але індекси обчислюються таким чином, що кожне місце записується лише одним потоком".

У цей момент ви можете або вкусити кулю, і додати трохи непотрібної синхронізації, або переробити код таким чином, щоб компілятор міг бачити його правильність (часто це можливо, іноді досить важко, іноді неможливо), або ви кинете в unsafeкод. І все-таки це додаткові ментальні витрати, і Руст не дає жодних гарантій на правильність unsafeкоду.

Різниця: менше інструментів

Через вищезазначені відмінності, в Rust набагато рідше пишеться код, який може мати перегони даних (або використання після безкоштовного, або подвійне вільне, або ...). Незважаючи на те, що це приємно, але прикро, що екосистема для відстеження таких помилок є ще більш розвиненою, ніж можна було б очікувати, враховуючи молодь та невеликий розмір громади.

Хоча такі інструменти, як valgrind та дезінфікуючий потік LLVM, в принципі можуть бути застосовані до коду Rust, чи це насправді все ще працює від інструменту до інструменту (і навіть ті, які працюють, можливо важко налаштувати, тим більше, що ви не можете знайти будь-які оновлення -надавати ресурси про те, як це зробити). Це не дуже допомагає, що Русту в даний час не вистачає реальної специфікації, зокрема формальної моделі пам'яті.

Коротше кажучи, unsafeправильно писати Rust-код важче, ніж правильно писати код C ++, незважаючи на те, що обидві мови приблизно порівнянні за можливостями та ризиками. Звичайно, це слід зважити на той факт, що типова програма Rust буде містити лише відносно невелику частину unsafeкоду, тоді як програма C ++ - це повністю C ++.


6
Де на моєму екрані перемикач +25 на зворотній зв'язок? Я не можу його знайти! Ця інформативна відповідь високо цінується. Це не залишає у мене явних питань щодо питань, які він охоплює. Отже, до інших моментів: Якщо я розумію документацію Руста, Rust має [a] інтегровані засоби тестування та [b] систему побудови під назвою Cargo. Чи готові вони на виробництво готові? Крім того, щодо Cargo, чи добре з гумором дозволити мені додавати оболонки, сценарії Python та Perl, компіляцію LaTeX тощо, до процесу збирання?
вт

2
@thb Тестуючі матеріали дуже голі кістки (наприклад, не глузуючі), але функціональні. Вантаж працює досить добре, хоча його спрямованість на іржу та відтворюваність означає, що це не найкращий варіант для покриття всіх кроків від вихідного коду до кінцевих артефактів. Ви можете писати сценарії побудови, але це може не відповідати всім згаданим вами речам. (Однак люди регулярно використовують сценарії побудови для збирання бібліотек C або пошуку існуючих версій бібліотек C, щоб це не було, як Cargo перестає працювати, коли ви використовуєте більше, ніж чистий Rust.)

2
До речі, для чого це варто, ваша відповідь виглядає досить переконливо. Оскільки мені подобається C ++, оскільки C ++ має гідні можливості для майже всього, що мені потрібно зробити, оскільки C ++ стабільний і широко використовується, я до цього часу був задоволений використанням C ++ для всіх можливих нелегких цілей (я ніколи не розвивав інтересу до Java , наприклад). Але зараз у нас є паралельність, і мені здається, що C ++ 14 бореться з цим. Я добровільно не пробував нову мову програмування протягом десятиліття, але (якщо Haskell не здасться кращим варіантом), я думаю, що мені доведеться спробувати Rust.
вт

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що означає, що за замовчуванням структура, що містить їх, не матиме жодного.
Hauleth

@ ŁukaszNiemier Це може статися гаразд, але є мільярд способів, коли небезпечний тип використання може закінчитися Sendабо Syncнавіть незважаючи на те, що це справді не повинно.

-2

Іржа також дуже схожа на Ерланг і Го. Він спілкується за допомогою каналів, які мають буфери та умовне очікування. Як і Go, він знімає обмеження Erlang, дозволяючи вам робити спільну пам'ять, підтримувати атомний підрахунок посилань і блокування та дозволяючи вам передавати канали від потоку до потоку.

Однак Іржа піде на крок далі. Поки Go довіряє вам зробити правильну справу, Раст призначає наставника, який сидить з вами та скаржиться, якщо ви намагаєтесь зробити неправильну справу. Наставник Руста - компілятор. Він робить складний аналіз, щоб визначити право власності на значення, які передаються навколо потоків, та забезпечити помилки компіляції, якщо є потенційні проблеми.

Далі йде цитата з документів RUST.

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

Якщо Ерланг драконічний, а Го - вільна держава, то Руст - це няня.

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


2
Ласкаво просимо на обмін стеками! Зауважте, що щоразу, коли ви посилаєтесь на свій власний блог, вам потрібно це чітко зазначити; див. довідковий центр .
Глорфіндель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.